Repository navigation
Integrate main with the three feature lines (Q13) - #4
Merged
Merged
Conversation
…terate before main merge
…iction patch, python wrapper Implemented via Cursor delegation, VERIFIED by Claude (build clean, 386/386 ctest green). Deliverables (spec docs/design/VLA_THESIS_PLANT.md): - seven_dof: direct CmdL1 per-wheel torque path (T_net = motor-brake-fx_kin·Re -> spin ODE), bypassing the throttle/drivetrain map -> native MF96 combined-slip = physical κ + grip loss (the #0 crux; verified at seven_dof_dynamics.cpp:611). plant_path disables the vehicle kinematic blend (tyre VLOW only). drive_split_front in params/yaml. - create_friction_patch_ground (per-wheel contact-x μ). wheel_mu() getter (L2 override). - SimSession.direct_control_path (lockstep, no cascade/actuator). pybind make_vla_plant_session, make_init_state_6dof, CmdL1/CmdL3. - configs ioniq5_awd.yaml + ioniq5_pacejka.yaml (per-wheel B=Ca/2 verified, mean B≈14.2). - python/vdsim_plant.py (boundary validation + clear errors) + README + examples/vla_plant_demo.py. - tests/integration/test_vla_plant.cpp (CmdL1 not throttle-mapped, friction patch, headline brake+turn on patch). Cursor also fixed a dynamic-steering regression it found. BETA — to evaluate + iterate before any main merge (this is the first client/beta tester). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…eptance [BETA iterate] Beta evaluation found a real model defect: in the direct-L1 path a constant brake_torque kept decelerating the wheel past omega=0 -> the wheel spun BACKWARD -> kappa ran to -31 (garbage GT) under hard braking. Fix: brake opposes rotation and vanishes at rest (tanh(omega/eps)), so a locked wheel settles at kappa~-1 (physical) instead of -inf. Strengthen the acceptance smokes (Cursor's were loose "0.7*cap OR slip"): - CmdL1DirectTorqueNotThrottleMapped: prove BYPASS (CmdL1 drive torque accelerates; CmdL4 throttle=0 does not). - CombinedBrakeTurnGripLossOnPatch (headline): GT friction-circle invariant ||[Fx,Fy]||<=mu*Fz every step + real slip (lock) + brake-no-reverse (kappa>-1.2) + vehicle-level grip loss (|sumFx| < commanded). (Over-brake LOCKS -> force on the MF sliding tail below peak, ABS territory — so we assert engagement+grip-loss, not peak.) - GroundTruthConsistency: sum Fy_body ~ m*ay + every wheel inside the circle. 388/388 ctest green. Branch VDSim-Thesis (beta). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…me gates [BETA iterate] Acceptance #1 + #5 verification: - DryHandlingMatchesLinearBicycle: 7DOF steady yaw rate vs the linear single-track model with the same Caf/Car. Measured ratio 0.963 (within 4%) -> confirms the plant reproduces the controller's bicycle handling AND the MF96 B=Ca/(C*D) calibration recovered Caf/Car (per-wheel B=Ca/2). Gated at 15%. - FasterThanRealtime: 5 s plant trajectory at substep 5e-4 = ~23 ms wall (gated <500 ms). 390/390 ctest green. Branch VDSim-Thesis (beta). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…gates (390 green)
…les) Add docs/VDSIM_PLANT_TUTORIAL.md — a step-by-step manual a client can follow: build, import (sys.path), quickstart, the obs schema (units/frames), control [delta, Fx_total], low-mu patches, full closed-loop drop-in, the ioniq5_awd.yaml + tyre YAML explained (incl. the Ca->B_lat mapping for new vehicles), API reference, the validated guarantees, tips/gotchas, and a troubleshooting table. Verified the demo + paths run (python3). README now points to it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…fness + peak-mu [BETA iterate]
Replace the plant tyre MF96 -> MF2002 .tir backend (Cursor, VERIFIED by Claude). MF96 lacked
load dependence of the Pacejka coefficients; MF2002 adds it. Public-synthetic Ioniq5 .tir
calibrated (PKY1=-24.6, PKY2=2.56, PDY1=1.0, PDY2=-0.10, PCY1=1.3; PDX1=1.0, PDX2=-0.10):
- single .tir, but load-dependent Kya(Fz) recovers per-wheel cornering stiffness from the
front/rear static Fz: Kya(7011)=109.3k, Kya(4558)=79.5k -> axle Caf~218k / Car~159k
(target 220/160, ~1% — more physical than MF96's mean B).
- cornering stiffness now CONCAVE in load: Kya(2*Fz0)/Kya(Fz0)=1.435 (<2) — the saturation
MF96 missed (linear Ca). Peak mu falls with load: 0.90 -> 0.81 at 2*Fz0 (PDY2=-0.1).
- friction patch still works (contact mu=0.5 -> peak 0.50; MF2002 honours Lmuy=LMUY*mu_lat).
Wiring: ioniq5_pac2002.{tir,yaml} (backend mf2002 + tir_path), ioniq5_awd.yaml -> mf2002 tyre,
.gitignore exception, vdsim_plant.py resolves tir_path, pybind exposes backend/tir_path. MF96
kept available. 4 new MF2002 tests (real-tyre dry==bicycle, load-sensitivity, patch-halves-grip,
combined grip-loss). 394/394 ctest green. Branch VDSim-Thesis (beta).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Tyre section now describes the MF2002 .tir backend (load-dependent Pacejka coefficients) that replaced MF96: load-shaping coeff table (PKY1/PKY2 stiffness saturation, PDY1/PDY2 peak-mu load sensitivity), single-.tir per-axle Ca recovery from static Fz (Kya(7011)=109.3k->Caf~218k, Kya(4558)=79.5k->Car~159k), concavity Kya(2Fz0)/Kya(Fz0)=1.435, peak mu 0.90->0.81. Validation section: 394/394, dry==bicycle for real .tir, load-dependent checks. Branch VDSim-Thesis (beta). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Self-contained handover for the first beta customer (VLA thesis): setup, plant-swap recipe for closed_loop_sim.py, obs->MPC state mapping, the MF2002 load-dependent tyre change tied to the sigma_Fz / chance-constraint story, validation status, and the beta-feedback ask. Branch VDSim-Thesis. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…+ useGT>1 analysis Thesis-side beta #1 came back: integration success, slip-angle cleanly separates det 8.3deg (useGT 1.08) vs antic 2deg (useGT 0.91) — load-dependent MF2002 sharper than the bicycle plant. Two follow-ups recorded: - item 0 (requested): tutorial gets a conda/venv rebuild section + notes VDSIM_BUILD_PYTHON defaults OFF. Shipped .so is cpython-38; an external MPC env (conda py3.11 acados/cvxpy) needs a rebuild with -DPython3_EXECUTABLE / -Dpybind11_DIR for that interpreter (ABI lock). - useGT>1 diagnosed (NOT a friction-circle violation): wheel.mu reports the contact/road mu, not the MF2002 realized peak muy(Fz)=(PDY1+PDY2*dfz)*LMUY*mu_contact. PDY2<0 => a lightly loaded wheel (dfz<0) has muy>mu_contact, so |F| can reach ~1.08*mu_contact*Fz. Tyre stays in its own load-dependent ellipse; the metric used nominal mu in the denominator. Fix options (A expose realized peak coeff / B docs-only) recorded; awaiting decision. Branch VDSim-Thesis (beta). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…n A) [BETA iterate] Thesis beta #1 saw useGT=||[Fx,Fy]||/(wheel.mu*Fz)=1.08 (>1). Diagnosed (Claude): not a friction-circle violation — wheel.mu is the road/contact mu, but the MF2002 realized peak muy(Fz)=(PDY1+PDY2*dfz)*LMUY*mu_contact rises ABOVE nominal at low load (PDY2<0), so a lightly loaded wheel carries ||F|| > mu_contact*Fz while staying inside its own load-dependent ellipse. Fix (Cursor, verified): add per-wheel realized peak coefficient. - ITireModel::Output/Wrench gain mu_peak; each backend compute() fills it (MF2002 max(|mux|,|muy|); MF96 max(D_long*mu_x, D_lat*mu_y); linear max(mu_long,mu_lat)), evaluate() propagates to the wrench. - IVehicleDynamics::wheel_mu_peak() (Ld2 override), pybind, plant obs wheel[i]["mu_peak"]. - wheel.mu (road mu) preserved. Docs: use mu_peak for the friction circle. Verified end-to-end through the python plant (combined brake+turn on mu=0.5 patch): max useGT(old contact mu)=1.046 (reproduces the artifact) -> max useGT(mu_peak)=0.998 (<=1). New test VlaPlant.RealizedPeakMuBoundsFrictionCircle. 395/395 ctest green. Branch VDSim-Thesis. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… with thesis-side) Thesis-side independently reached the same mu_peak fix (det useGT 1.08->1.00, matching the plant probe 1.046->0.998; PDX==PDY so it saturates at exactly 1 at the limit) and confirmed mu_peak-based friction-utilization fits their P1 gate. Both items closed, no main-merge blocker. Add a durable Changelog (BETA #1 delivery / BETA #2 feedback round) since the mu (contact) vs mu_peak (realized) semantics is subtle to track across iterate rounds. Branch VDSim-Thesis. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…tance) Thesis-side low-priority re-request to expose wheel.grip_usage=||F||/(mu_peak*Fz) declined: plant stays raw-GT-only (usage metric is thesis-side's; tutorial §4). Recorded to avoid re-litigating next round. Branch VDSim-Thesis. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…utilization caveat The metric name was misleading: "GT" is the thesis-side GT/Dem source tag (no Dem counterpart in VDSim, so meaningless to a generic customer), and "use" implies utilization — but the MF force-slip curve peaks then falls, so sat=||F||/(mu_peak*Fz) is non-monotone: =1 only AT peak slip, <1 on both the rising side (reserve) and the sliding side (drift, past peak). A bare sat<1 is ambiguous, so it must be paired with the slip channel to tell reserve from departure. Rename to "friction saturation ratio (sat)" in tutorial §4 + README and add the disambiguation table. Historical thesis-side useGT quotes kept as record. Thesis-side code var rename (useGT/useDem -> satGT/satDem) is their repo (VDSim exposes no metric var — raw GT only). Branch VDSim-Thesis. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… operating-point characteristics Replace the derived "sat" ratio guidance with raw tyre operating-point characteristics, so the analyst computes any metric and — crucially — can tell rising vs sliding (drift) unambiguously, which a single sat scalar cannot (force-slip curve is non-monotone past the peak). Per wheel the plant now gives capacity (mu_peak) + peak location (alpha_peak lateral, kappa_peak long) + current position (alpha, kappa). |alpha| vs alpha_peak => reserve / at-limit / drift. - mf_peak_slip(B,C,E) (interfaces.hpp, shared): slip at D*sin(C*atan(phi)) peak via Newton on (1-E)u+E*atan(u)=tan(pi/2C); C<=1 or B~0 -> inf (no interior peak). - ITireModel::Output/Wrench + IVehicleDynamics::wheel_alpha_peak()/wheel_kappa_peak() (Ld2); MF2002 (By/Cy/Ey, Bx/Cx/Ex), MF96 (B/C/E_lat/long), linear (inf). pybind + plant obs. - Test PeakSlipMatchesNumericalArgmax (model vs Fy/Fx argmax, Fz 3000/5764/9000, 5%). 396 green. Verified through the python plant: gentle turn |alpha|0.011<alpha_peak0.140 (reserve); brake+turn on mu=0.4 |alpha|0.162>alpha_peak0.065 (drift). Dry alpha_peak~=8.0deg matches the earlier det slip 8.3deg (=at peak). Branch VDSim-Thesis. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ned-slip analysis The combined-slip peak is direction-dependent (no single scalar), so instead of baking it in, let the analyst reconstruct the exact combined force surface with the tyre evaluator that is already bound (create_magic_formula_tire_from_tir -> ITireModel.compute). Better than dumping MF coefficients: no confidentiality leak (.tir stays in the engine), no re-implementation drift. - TireOutput now also exposes mu_peak / alpha_peak / kappa_peak (was Fx/Fy/Mz only), so a standalone compute() returns capacity + pure peak slips too. - Tutorial §4: offline tyre-query recipe (combined peak along a slip direction, friction ellipse). Verified: pure-lat peak alpha=0.130; at kappa:alpha=1 combined peak alpha=0.079. 396/396 green. Branch VDSim-Thesis. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…re_model)
Previously the only way to query the tyre offline was a fresh create_..._from_tir instance; the
live model the dynamics runs was not reachable (owned by the dynamics, no accessor). Add the
chain: IVehicleDynamics::tire() (const ITireModel*, default null; Ld2 returns its tire_.get()),
pybind .def("tire", ..., reference_internal), and plant.tire_model property. Now the analyst
queries the exact running tyre (single source of truth, no .tir path juggling) for friction
ellipse / combined-slip peak. Verified: plant.tire_model.compute == fresh instance (identical
force law). 396/396 green. Branch VDSim-Thesis.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
….params) For user data delivery only — the simulation runtime stays flat (VehicleParams + a tyre model in the dynamics). Add a read-only Python facade mirroring vehicle -> part -> physics: plant.vehicle.params (VehicleParams), plant.vehicle.tire.params (TireParams), plant.vehicle.tire.model (live ITireModel; plant.tire_model kept as a shortcut). No C++/runtime change. Verified the path returns the live model (Fy/alpha_peak match). Branch VDSim-Thesis. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Leave a TODO (code + roadmap) to extend plant.vehicle.<part>.{model,params} beyond the tyre to
brake/steering/drivetrain/suspension/ARB. Why: only the tyre has a live model handle today; the
rest are pluggable modules (set_*_module, no getter) or flat VehicleParams fields. Path: add
const module getters on IVehicleDynamics (mirror tire()), bind, add a _PartView each.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…_CUSTOMER_FEEDBACK
…ansition + steering lag docs - P1a: roll_angle_qs() / pitch_angle_qs() → obs['roll'], obs['pitch'] [rad] - P1b: 2D polygon friction map (point-to-polygon distance, blend_distance=1.0m, min-μ overlap rule) - P2a: 1D friction_map smooth transition (boundary ±1.0m linear interpolation) - P2b: steering actuator lag documentation + vp.steer_deadtime_s implementation in SimSession - Tests: PolygonFrictionInsideAndOutside, PolygonFrictionBlendZone, FrictionPatchSmoothTransition, roll/pitch obs, polygon friction map - Docs: VDSIM_PLANT_TUTORIAL.md §4 (roll/pitch), §6b (2D polygon), §7 (steering lag), HANDOFF.md updated - Build: 399/399 ctest green (396 + 3 new)
… ladder Port thesis core improvements for main cherry-pick: TireSetup per-axle/per-wheel + catalog/cosim/pybind + tests; IVehicleDynamics::tire(wheel); make_direct_control_session + FrictionMapConfig; steer_deadtime via ActuatorModel; ladder_lowering.hpp; SimOutput wheel GT fields.
Thesis-only plant wrapper: TireSetup, make_direct_control_session, _obs_from_output(sess.output()) slim path.
Add STRUCTURED_REVIEW spec, evidence bundle tool, and review report. Fix build_complete, part compat/cards, plant_path default, VLA smoke.
Svelte 5 scaffold (Scenario/Run/Build/Analyze), backend APIs, eval/capture/parity tools, and evidence. GUI tier frozen after this commit until scenario-authoring adoption failures.
D2 replaces the horizontal projection of gravity with an exact road-frame resolution and routes the banked-turn centripetal term through the normal load path instead of an equivalent-ay postprocess. - seven_dof: build x_road/y_road from the injected unit normal (no small-angle linearisation); gravity components are taken along that frame, and the planar residual subtracts the legacy Fz*n term so D1's wrench is not double counted. - seven_dof/fourteen_dof: N/m = g*n_z + (v_x*r)*n_y(body) drives Fz, so a pure grade still gives m*g*cos(theta) and a bank redistributes load in the force path. v_x*r is frozen per RK substep to keep the +/-bank mirror exact. - L4 contact height keeps flowing through z_u -> wheel_travel -> the existing camber/toe sweep; no layer-specific transform was added. Analytic gates (raw relative error, printed by the tests): - static grade 12 deg: a_x rel 1.15e-14, sum Fz rel 2.53e-16 (L3 and L4) - banked limit speed 10 deg / mu 0.80 / R 60 m: rel 1.37e-16, vertical residual -5.46e-12 N (L3 and L4) D0 flat baseline stays bit-identical on L3 (max abs diff 0). The D1 bank vertical-delta expectation moves 436.5208463 -> 435.6222462 N because gravity is now resolved on the exact road-y basis; the D1 closure itself is unchanged.
A trace could not say whether it recorded the plant under verification or
the same code used as an optimiser's internal prediction model. With the
Jacobian in-loop mode the two are the same object, so the hazard is not
in-loop use itself but an undeclared dual role: a run whose plant and
predictor share parameters, presented as a verification result.
manifest.role in {plant, predictor} makes that mechanically detectable
alongside param_hash, which already covers plant parameters only.
- writer emits schema_version 0.2 and role on every trace. role is a
required keyword-only TraceWriter argument with no default, because a
default is exactly the silence the field exists to break.
- VDSimPlant.enable_trace defaults to role="plant" — that class is the
plant; a caller driving it as a predictor passes role explicitly.
- reader resolves role once at open: missing at 0.1 means plant plus one
warning, missing at 0.2 is an error, an unknown value is an error at
either version.
- 0.x readability is now an explicit list rather than an exact-minor
match, so 0.1 stays readable only because the fallback above exists.
- golden fixture regenerated at 0.2; golden_v0_1.vdtrace is frozen in
place as the artefact that keeps the fallback path under ctest.
- renderer prints the role in the HUD and never branches on it.
Contract: 11_trace_contract_spec.md 3.1 (decided 2026-09-02).
ctest 408/409 in the worktree build; the single failure, cmrosif_compat,
is a pre-existing gap on this branch line (its test script is untracked
here) and is unrelated to this change.
The renderer had one hardcoded screen: the panels came from a fixed PREFERRED_SERIES list and every BEV layer was always on. Deciding what a run should show meant editing the renderer. A preset is renderer configuration, not trace content, so this does not move schema_version. It declares panel channels, labels, y-ranges and BEV layer visibility — that is the whole MVP surface; free-form layout editing stays out of scope. - vdsim_preset: built-in overview (BEV, driven path, optional reference path, speed, steer and longitudinal command), YAML/JSON user presets layered over a built-in via extends, and CLI > user preset > built-in resolution. - control, tire_limit and road_contact are refused rather than stubbed. A stub would let --preset tire_limit render something that is not the contracted screen, and road_contact would advertise contact data the core does not resolve yet. - a channel the trace lacks drops its panel silently, so one preset works across runs recorded with different channel subsets; required: true is the opt-in that turns the omission back into an error. A trace with no plottable channel renders BEV-only instead of failing on an empty grid. - panel geometry is a function of the panel count alone, and layout_violations() checks the drawn figure for text, legend or panels leaving the frame. render() returns the list, so the frame guarantee is not left to eyeballing a GIF. - panel series now carry preset labels, so raw channel lookups such as u_steer read the stashed channels directly; going through the labelled series would have straightened the front wheels on a relabelled panel. Contract: 11_trace_contract_spec.md 6.3 (approved 2026-09-02). New ctest render_presets (54 checks). ctest 409/410 in the worktree build; the one failure, cmrosif_compat, is pre-existing on this branch line (its test script is untracked here) and unrelated.
…onfig Q1' of the 2026-09-04 PO briefing: the recorded ctest count is only meaningful for one named configuration. Two registrations made that count depend on the checkout instead of the configuration. - VDSIM_BUILD_GUI_V3_E2E (default OFF) gates gui_v3_e2e. GUI v3 is a deferred node-stack component, so its end-to-end test is out of scope for the canonical build (07_p0_demo.md 6.3). - cmrosif_compat is registered only when its driver script exists in the tree. It was registered unconditionally while the script is absent on this branch, so it could never pass here. No test logic changes.
VALIDATION.md carried 409/409 measured in a configuration nobody could name,
so the number went stale silently. Fix the record and make staleness a build
failure rather than a reading-comprehension exercise.
- CMakePresets.json: `validation` preset is the single canonical configuration
(Release, PYTHON=ON, ERGACCESS=OFF, GUI_V3_TESTS=OFF).
- docs/VALIDATION.md: machine-readable currency block carrying all four
required elements (tests / config / commit / date) plus the excluded
targets. The three prose copies of the count now name the preset.
ISO figures and the benchmark matrix are untouched.
- scripts/check_validation_currency.py: parses the block and compares every
element against `ctest -N` and a ctest run log. Test names are read to end
of line so gtest value-parameterised names ("... <00-00 00-00>") count.
- tests/scripts/test_validation_currency.py: 22 cases, registered as the
`validation_currency` ctest, so a broken gate fails the suite.
- build.yml: validation job runs the preset and the gate.
- VDSIM_BUILD_GUI_V3_E2E -> VDSIM_BUILD_GUI_V3_TESTS (default OFF), now
covering gui_v3_api_smoke as well.
Refs VDSimMarketing 07_p0_demo.md 6.4/6.5, 18_dev_briefing_0903.md Q1'/Q1''.
The validation numbers were measured with whatever CMake happened to be on the measurement host, while CI ran on a runner label (ubuntu-20.04) that no longer exists -- every build.yml run since 2026-05-28 was cancelled without ever being assigned a runner, so the canonical job was not measuring anything. - move both jobs to ubuntu-22.04 and refresh the compiler matrix to the toolchains that image actually ships (gcc-11, clang-14) - pin CMake to one exact version (3.31.10) in both jobs instead of relying on the runner image default; CMakePresets.json is schema v3 and needs >= 3.21 - add `toolchain:` as a required element of the VALIDATION.md currency block and fail the gate when the recorded version differs from the one that performed the measurement, so a toolchain bump cannot land without the numbers being re-measured in the same commit - re-measure on the pinned CMake: 407/407, three consecutive runs - drop the stale hand-written counts from README/CONTRIBUTING; the suite count lives in docs/VALIDATION.md only
Ancestry alone was too weak a contract for the commit: line. A document can never record the sha of the commit that contains it, so the gate has to accept an ancestor -- but any commit in between may add or remove a test registration, which invalidates the recorded count while every other element still matches. The gate now also requires the registration surface (CMakePresets.json, every CMakeLists.txt, cmake/**) to be byte-identical between the recorded commit and the measured one. Test sources stay off that surface on purpose: their effect is already caught by the passed/total comparison. Every failure now prints the re-measurement command, because an unexplained gate failure gets "resolved" by deleting the gate. VDSimMarketing 18_dev_briefing_0903.md 10.1 Q1b / 07_p0_demo.md 6.6 (3).
- delete .cursor/rules/{00-personalize,copyedit-en,copyedit-ko}.mdc
(00-personalize @-includes /home/ailab-12/cursor_system/..., host-specific)
- delete docs/design/CURSOR_USAGE.md (personal workflow + subscription pricing;
it was published by mkdocs since docs_dir=docs)
- drop the dangling CURSOR_USAGE reference from .cursor/rules/vdsim.mdc
- CLAUDE.md: drop the ~/.claude dependency and the demand-gate governance block
(it cited private VDSimMarketing paths); docs/HANDOFF.md pointer removed with it
- .gitignore per-agent config (AGENTS.md, personal .cursor/rules/*.mdc)
(cherry picked from commit 5ce5b60)
- delete .cursor/rules/{00-personalize,copyedit-en,copyedit-ko}.mdc
(00-personalize @-includes /home/ailab-12/cursor_system/..., host-specific)
- delete docs/design/CURSOR_USAGE.md (personal workflow + subscription pricing;
it was published by mkdocs since docs_dir=docs)
- drop the dangling CURSOR_USAGE reference from .cursor/rules/vdsim.mdc
- CLAUDE.md: drop the ~/.claude dependency and the demand-gate governance block
(it cited private VDSimMarketing paths); docs/HANDOFF.md pointer removed with it
- .gitignore per-agent config (AGENTS.md, personal .cursor/rules/*.mdc)
(cherry picked from commit 5ce5b60)
- delete .cursor/rules/{00-personalize,copyedit-en,copyedit-ko}.mdc
(00-personalize @-includes /home/ailab-12/cursor_system/..., host-specific)
- delete docs/design/CURSOR_USAGE.md (personal workflow + subscription pricing;
it was published by mkdocs since docs_dir=docs)
- drop the dangling CURSOR_USAGE reference from .cursor/rules/vdsim.mdc
- CLAUDE.md: drop the ~/.claude dependency and the demand-gate governance block
(it cited private VDSimMarketing paths); docs/HANDOFF.md pointer removed with it
- .gitignore per-agent config (AGENTS.md, personal .cursor/rules/*.mdc)
(cherry picked from commit 5ce5b60)
Integration merge #1 of 3 (PO Q13, integration/p0-merge). Conflict resolution (20 files, 57 hunks): - code: main's SessionKind supersedes the feature line's bool direct_control_path (same feature, two names); main's skip_kinematic_blend() supersedes vp_.plant_path (superset); the feature line's D1/D2 contact-frame work is kept verbatim. - build: union of both test/target lists. - measurement metadata: docs/VALIDATION.md counts set to PENDING; no number is carried over from either side. - physical baselines: 0 conflicting hunks (ISO/benchmark/analytical values untouched).
Integration merge #2 of 3 (PO Q13). Single conflict in python/vdsim_render.py: line A T6 instrument strip and line B preset BEV-layer toggles are different features on the same two functions, so both are kept — _setup_axes keeps the fixed-rect T6 layout and gains line B zero-panel fallback, _draw_static takes both ax_instruments and bev.
Integration merge #3 of 3 (PO Q13). Conflicts: 4 files, 10 hunks. - .github/workflows/build.yml: union — main's Python/pybind11 install and line C's pinned CMake are independent prerequisites of the same job. - cosim/CMakeLists.txt: union — main's ac_fast_lane_importer and comms_tx_templates kept, cmrosif_compat takes line C's EXISTS guard. - docs/ROADMAP.md: union of the content rows; every hardcoded ctest count is dropped in favour of line C's link to docs/VALIDATION.md. - docs/VALIDATION.md: no number from either side. The currency block goes to the PENDING state — the 407 was measured on a tree that no longer exists, so it is void, not stale. scripts/check_validation_currency.py learns PENDING as an explicit pre-measurement state so the gate can stay green through the merge without being deleted or bypassed; it demands a 'pending:' reason, and the duplicate-count scan over docs/ stays active. 47 unit checks pass.
Three post-merge defects, all real, none worked around by exclusion.
1. Build-directory name was hardcoded. The "validation" preset builds into
build-validation, but cosim/CMakeLists.txt spelled ${CMAKE_SOURCE_DIR}/build
into every Python test's PYTHONPATH and tools/module_workshop.py looked for
build/lib/libvdsim_core.a. Both now follow ${CMAKE_BINARY_DIR}, handed to
the tests as VDSIM_BUILD_DIR; the documented "cmake -B build" flow is
unchanged. This is why the preset previously only worked next to a stale
build/ directory.
2. D1ContactFrame.InvalidContactResetsTireTransientBeforeRecontact compared a
warm level against one rebuilt from its State snapshot. That is an exact
clone only if the whole history is in State, which stopped being true at L4
when main added KC bushing compliance (history lives in the axle DAE). The
check now measures that reconstruction residual on a control run that never
loses contact and requires the airborne run to be no worse, instead of a
tolerance fitted to the observed difference. L3 stays exact.
3. tests/fixtures/d0_flat_contact_l4_baseline.csv re-captured. Attribution was
measured, not assumed: suppressing compliance_targets_rad() makes the merged
tree reproduce the pre-merge L4 fixture bit-for-bit, so main 9a40c1b (KC
bushing compliance) moved it and the contact-frame work did not. L3 is
untouched and still bit-identical.
The validation job built the Python bindings without pybind11 and ran the catalog/plant/trace tests without numpy, pyyaml or matplotlib. The trace and render suites arrive with this merge, so matplotlib and pillow join the build job pin as well.
VlaPlant.FasterThanRealtime asserted a fixed 500 ms wall-clock budget for 5 s of simulated time. That budget is the Release contract; an unoptimised Debug build runs the same code 3-6x slower, so both Debug jobs failed on a number that says nothing about the plant. Release passed. The budget is now read from VDSIM_PERF_BUDGET_S, the variable the Python suite already used, with the parsing in one place (tests/support/perf_budget.hpp). Unset, empty and malformed all mean "not stated" and fall back to the compiled default, so a broken CI expression cannot silently disable the assertion. The Python side is reworded to follow the same rule. CI injects the value on the Debug row only. Release states nothing and keeps the strict default, which is where a real performance regression is detected. docs/VALIDATION.md records what blocked the previous canonical measurement. The currency block stays PENDING: the figure is not canonical until all five jobs pass three times in a row.
The three-run CI measurement on integration/p0-merge (run 34035244533, attempts 1-3) reports 485/485 on all five jobs, so the currency block moves from PENDING to the five required elements. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Integration of the three independent feature lines onto
main(PO Q13, briefing §14).mainand the feature lines had diverged in both directions since34db50c, so theresolution happens here rather than on
main.What is merged
feat/scene-sensors-parserfeat/render-preset-overviewdocs/validation-canonical-configvalidationpreset, currency gate, pinned CMake, GUI v3 test switchchore/repo-hygieneis already contained in line A and needs no separate merge.Conflict hunk classification
PENDINGmain's generalisation kept (SessionKindoverdirect_control_path,skip_kinematic_blend()overvp_.plant_path)docs/evidence/review/2026-06-21/**)main-lineage record kept; the thesis-branch copy of the same filename documents a tree not in this historyTotal 67 hunks over 21 files across the three merges.
Test counts
docs/VALIDATION.mdcarries no number. The last canonical figure was measured on a treethat no longer exists, so it is void rather than stale, and the currency block sits in an
explicit
PENDINGstate with the reason recorded. The gate learnedPENDINGas a state(it demands a
pending:line, and the duplicate-count scan overdocs/stays active) sothat the block did not have to be deleted or bypassed to get through the merge.
Post-merge fixes in this PR
buildin the ctestPYTHONPATHand intools/module_workshop.py, so the canonical preset — which builds intobuild-validation— only worked next to a stalebuild/. Both now follow${CMAKE_BINARY_DIR}.D1ContactFrame.InvalidContactResetsTireTransientBeforeRecontactrebuilt a level froma
Statesnapshot and required an exact match. That stopped being exact at L4 whenmainadded KC bushing compliance, whose history lives in the axle DAE rather than inState. The check now measures that reconstruction residual on a control run that neverloses contact and requires the airborne run to be no worse — no tolerance was fitted to
the observed failure. L3 remains exact.
tests/fixtures/d0_flat_contact_l4_baseline.csvre-captured, with the cause measuredrather than assumed: suppressing
compliance_targets_rad()makes this tree reproducethe pre-merge L4 fixture bit-for-bit, so
main9a40c1bmoved it and the contact-framework did not. The L3 fixture is untouched and still bit-identical.
validationCI job built the Python bindings with no pybind11 and ran thecatalog/plant/trace tests with no numpy, pyyaml or matplotlib. Both jobs now install the
same pinned set.
Local result
Clean configure + build +
ctest --preset validationon ailab-12: 485 registered,485 passed, 0 failed. That number is deliberately not written into the document — the
canonical measurement is the CI validation job on this PR.
🤖 Generated with Claude Code