Skip to content
OnlytiPublic

About

No description, website, or topics provided.

Resources

Contributing

Stars

2 stars

Watchers

0 watching

Forks

Latest commit

 

History

752 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

VDSim

English · 한국어

build license status tests

Experimental / pre-release — not for production. Evidence and limits: VALIDATION.md (v0.5.1+). Validated: analytic + ISO maneuvers + L1↔L3 self-consistency + pure-slip same-.tir cross-check (Chrono Pac02 ~0.8%) — not MF-Tyre product parity. NOT yet: full-vehicle commercial cross-val, real-vehicle data, production sign-off.

Grip-loss demo

Deterministic VDSim plant: controller hits an unseen low-μ patch, tyres saturate past peak (drift>1) — real grip loss, not a soft-clamp. Reproduce: python examples/demo_grip_loss.py (pip install vdsim[plot] or local wheel).

Experimental / pre-release software — not for production use. What is verified, and what is not yet: see docs/VALIDATION.md.

Verification scope. VDSim's tire-force layer is cross-checked against an independent implementation driven by the same .tir parameter file: Chrono Pac02 (BSD-3, ~0.8% / 0.7%). This is a pure-slip cross-check between implementations, not product parity. Full-vehicle behaviour (suspension, transient) and comparison against real-vehicle measurements are not yet covered.

Grip-loss demo

Deterministic VDSim plant: controller hits an unseen low-μ patch, tyres saturate past peak (drift>1) — real grip loss, not a soft-clamp. Reproduce: python examples/demo_grip_loss.py (pip install vdsim[plot] or local wheel).

Open-core vehicle-dynamics simulator. VDSim owns the vehicle — implemented L1–L5 dynamics with real Pacejka MF / LuGre / belt-transient tires, hardpoint suspension kinematics you can design against, and bidirectional FMI 2.0 — and delegates rendering/sensors to tools like CARLA. It is the chassis-accurate half that perception stacks lack.

Docs (theory + reports): https://onlyti.github.io/VDSim/ · all run modes (API / batch / FMI): docs/RUNNING.md · catalog & physics options: docs/CATALOG_AND_PHYSICS.md

Quickstart (pip wheel)

Prerequisites: Python 3.8–3.12 (pre-built wheels for Linux x86_64 and Windows AMD64). Install from PyPI (or build locally — see from source below).

pip install "vdsim[plot]"           # or a local wheel: pip install "./vdsim-*.whl[plot]"
vdsim-quickstart          # writes run.csv + run.png in the current directory

The package ships generic presets only; VDSimPlant() defaults to generic_sedan. A car whose parameters must stay private is kept outside the package and found first: export VDSIM_PRIVATE_CONFIGS=/abs/root with /abs/root/vehicles/<name>.yaml (tyre under /abs/root/parts/tire/), then VDSimPlant(config="<name>.yaml").

Install into a clean environment: another distribution also provides a top-level vdsim package, and if both are present that one wins the import. VDSim detects this and raises CoreShadowedError naming the file that was loaded.

Measured on a clean conda env (Python 3.11, Linux x86_64, 2026-06-25): first result in seconds — pip install [plot] + vdsim-quickstart → run.csv + run.png in ~6 s wall-clock (cold; lab network, matplotlib wheel included). Repeat quickstart ~1.5 s.

Verify the install:

python -c "import vdsim; from vdsim_lab import Sim; Sim(vehicle='sedan', level='L2')"

Source for the script: examples/quickstart.py — uses only from vdsim_lab import Sim, Road (no repo paths).

From source

Prerequisites: a C++17 compiler, CMake ≥ 3.16, Python ≥ 3.10.

  • Linux: g++ ≥ 9 or clang ≥ 10. CI builds gcc-11 and clang-14 on Ubuntu 22.04.
  • Windows: Visual Studio 2019+ with "Desktop development with C++" (MSVC).
  • Building the tree needs CMake ≥ 3.16, but the canonical validation run goes through CMakePresets.json (preset schema v3), which needs CMake ≥ 3.21 — an older CMake builds VDSim fine and simply cannot read the preset.

Python package (editable / local wheel build):

pip install ".[plot]"
python -c "import vdsim; print('ok')"

Full C++ tree (tests, real-time runtime, FMI, CARLA bridge):

cmake -B build -DVDSIM_BUILD_PYTHON=ON          # add -G Ninja on Linux
cmake --build build --config Release            # -j on Linux
cd build && ctest -C Release                    # binaries in build/bin/

The canonical suite count lives in one place only — docs/VALIDATION.md. Reproduce it with the pinned preset:

cmake --preset validation && cmake --build --preset validation
ctest --preset validation

Run an experiment in Python (write your own controller)

VDSim gives you the simulation seam; you own the loop and the algorithm. Build the plant straight from the core (vehicle / tire / level / road) — no scenario file, no network — then drive it: set_input(action) → run_core_dt(), read state() / measurements(id).

from vdsim_lab import Sim, Road, Sensors

sim = Sim(vehicle="sedan", level="L2", road=Road.iso8608("C"),
          sensors=Sensors().gnss(pos_std=0.3).imu(), v0=12.0,
          sensor_mounts={"gnss": {"type": "gnss", "pos": [1.4, 0, 1.0]}})

while not sim.done(12.0):
    st = sim.state()                                  # ground truth
    gnss = sim.measurements("gnss")                   # noisy, at the mount
    steer, throttle, brake = my_controller(st, gnss)  # <-- YOUR algorithm
    sim.set_input(steer=steer, throttle=throttle, brake=brake)
    sim.run_core_dt()                                 # advance one core step

sim.to_csv("run.csv")                                 # ground-truth + per-wheel log
sim.metrics(["peak_ay", "cte_rms", "lap_time"])       # scalar reductions
sim.plot("run.png", signals=("vx", "ay", "r", "xy"))  # optional (needs matplotlib)

Copy templates/experiment_template.py, replace controller(...), and run from a cloned tree:

PYTHONPATH=build/python:python python3 templates/experiment_template.py

Runnable examples (require clone + build): examples/experiment_quickstart.py, examples/experiment_path_follow.py (pure-pursuit + CTE). Pip users: use examples/quickstart.py / vdsim-quickstart instead. Full reference (the seam, Sim(...) options, evidence): docs/EXPERIMENT_API.md. This is the same set_input → tick seam the real-time server and batch runner use — in real-time mode the action arrives over UDP, here it comes from your function.

Visualization — Web GUI

python3 gui/server.py --port 8100        # Windows: python gui\server.py --port 8100

Open http://localhost:8100. The GUI auto-starts the real-time runtime and renders it: 3D view (orbit / chase / cockpit), road & terrain, per-wheel Fz / slip (κ, α) / tire-force vectors, and a telemetry HUD. Drive with the keyboard (↑↓ throttle/brake, ←→ steer) or a gamepad-wheel — force feedback via python tools/wheel_ffb_sdl.py --server <host> --udp-port 8101.

Visualization — headless trace render

A run can be recorded to a single .vdtrace file and rendered later, with no GUI, no node/npm and no browser. Recording is opt-in and off unless enabled:

plant.enable_trace("run.vdtrace", seed=0, run_id="demo")   # off unless called
...                                                        # step() as usual
path = plant.finalize_trace()
  • enable_trace(path, decimation=None, seed=None, run_id=None, producer=None, tags=None, role="plant") — one sample is offered per step(), taken before the step is integrated, so the pose is the state at t and u_steer / u_fx are the command held over [t, t+control_dt). Returns the resolved decimation.
  • decimation=None picks the smallest N whose record rate stays at or above 100 Hz (1 kHz control → N=10; a 20 Hz loop stays at N=1 and loses nothing).
  • finalize_trace() flushes channels, freezes the manifest and closes the file; it returns the written path (None when recording was never enabled).
  • The container is a zip: manifest.json + channels/*.f64 + overlays/*.json. That one file is enough to render — no re-simulation, no results file.
  • The manifest declares schema_version (the writer emits "0.5"; the fields each version added are listed below) and a required role, either "plant" (the simulator under verification) or "predictor" (the same code driven as an optimiser's internal model). enable_trace defaults to plant because VDSimPlant is the plant; a producer that builds a vdsim_trace.TraceWriter directly must pass role= — there is no default, so a predictor run cannot be recorded as plant evidence by omission. Reading a legacy 0.1 trace still works: a missing role resolves to plant with one warning. A 0.2 or later trace without one is an error. The renderer prints the role in the HUD and never branches on it.

Render presets

A preset decides which view of one trace is drawn. It is renderer configuration, not trace content, so presets never change what a .vdtrace holds. overview is the built-in — BEV, driven path, optional reference path, speed, steer and longitudinal command:

vdsim-render run.vdtrace                      # overview is the default
vdsim-render run.vdtrace --list-presets
vdsim-render run.vdtrace --preset my.yaml     # user preset (.yaml/.yml/.json)
vdsim-render run.vdtrace --panels speed,u_fx  # CLI wins over the preset
  • Resolution order: CLI option > user preset file > built-in default. A user preset only has to state what it changes; the rest is inherited from the built-in named by extends (default overview).
  • A preset declares panel channels, labels, y-ranges and BEV layer visibility. speed is derived from v_body; every other panel names a trace channel.
  • A channel the trace does not carry drops its panel silently — the same preset works on a run recorded with a channel subset. Mark a panel required: true to turn that omission back into an error.
  • Panel positions and the BEV axis coordinate system are fixed. Data autoscale is allowed; text, legend or panels leaving the frame is a failure and render() reports it in layout_violations.
  • control and tire_limit are contracted but not implemented, and road_contact is reserved until the core resolves full contact normals. Those names are refused rather than stubbed.
# my.yaml — everything not named here is inherited from overview
name: steer_only
panels:
  - channel: u_steer
    label: delta
    ylim: [-0.5, 0.5]
bev:
  waypoint: false
  view_half_m: 45.0

Scenario knowledge is attached after the run as overlays. VDSim validates the kind / name envelope and stores the object without interpreting it, so a newer producer can write overlays that today's renderer ignores:

import vdsim_trace
vdsim_trace.attach_overlay(path, {"kind": "path2d", "name": "intended_path",
                                  "xy": [[0.0, 0.0], [4.0, 0.0]]})
vdsim_trace.attach_overlay(path, {"kind": "region", "name": "mu_patch", "mu": 0.35,
                                  "polygon": [[40, -30], [60, -30], [60, 30], [40, 30]]})

The renderer takes one trace and writes a GIF plus a preview PNG (matplotlib + pillow, from the [plot] extra):

vdsim-render run.vdtrace --out run.gif --fps 20     # installed wheel
PYTHONPATH=python python3 -m vdsim_render run.vdtrace --out run.gif   # cloned tree

Options: --png preview path · --stride frame stride · --fps · --dpi · --title · --mp4 (only when imageio-ffmpeg is installed). Controller horizons add --sidecar / --view-half / --preview-frame (below).

The bird's-eye frame shows the reference path (dashed, from a path2d overlay; omitted when absent), the driven path, a body rectangle sized from manifest geometry, front wheels turned by the recorded steering command, a velocity arrow, per-wheel friction-utilization colour, a HUD in fixed screen coordinates, and the u_steer / u_fx time series with a current-time cursor. Axis limits come from geometry, never from autoscale, so the scale never drifts between frames.

The single-run view also carries two fixed-screen instruments: a steering-wheel dial whose rotation is the recorded road-wheel angle multiplied by manifest geometry.steer_ratio, and a speedometer in km/h. The speedometer scale is fixed for the whole trace (rounded up in 20 km/h steps), so neither needle changes meaning between frames. These instruments are part of the headless matplotlib output; they do not add node/npm/browser dependencies.

End to end — record, attach overlays, render:

PYTHONPATH=build/python:python python3 examples/demo_grip_loss.py \
    --out demo.gif --keep-trace

Overlaying several runs

Pass two or more traces and the renderer draws them in one view — one camera, one clock, a colour per run and the driven path in that same colour:

vdsim-render base.vdtrace tuned.vdtrace wet.vdtrace --out compare.gif \
    --labels "base,tuned,wet" --alpha 0.5
  • Runs are aligned by time, not by sample index: every run is interpolated at the frame time, so traces recorded at different dt, or of different length, still overlay correctly. Yaw is unwrapped first, so a heading near ±π does not swing the long way round.
  • A run whose trace ends early holds its final pose, fades to 35 % alpha, is marked (ended) in the HUD, and stops extending its path.
  • --alpha (body fill, default 0.55) and --path-alpha (default 0.9) set how much of an overlapped vehicle shows through. --colors overrides the palette, --labels the legend (default: manifest run_id, else the file stem).
  • Camera: --follow fit (default) is a static square window holding every route; --follow 1 tracks that run with the usual geometry-derived window.
  • --speed sets playback rate relative to wall-clock (--stride is single-run only). Each run's whole route is drawn faintly underneath; --no-ghost removes it.
  • Body colour is spent on run identity, so grip moves to the wheels: a tyre outline turns red once its utilization crosses 0.8. The right-hand panels overlay every run's u_steer / u_fx and max utilization on one shared time axis.
  • The preview PNG is taken at the frame of maximum divergence between runs, not at an arbitrary time.

End to end — the same manoeuvre at three friction levels, recorded to three traces and overlaid from the files alone:

PYTHONPATH=build/python:python python3 examples/demo_compare_runs.py --out compare.gif

Controller horizons — waypoint / target / prediction

Three curves get routinely conflated under the word "reference". They are different objects, and the renderer keeps them apart by name — in the code, in the sidecar keys and in the legend:

  • waypoint — the global route the run has to follow. Time-invariant, one per scenario, carried by a path2d overlay and drawn once as a dashed grey line. The overlay name may be waypoint, reference_path or ref_path; the first is preferred, the other two stay accepted for older traces.
  • target (MPC input) — the reference horizon handed to the controller at this step (N+1 points). Time-varying: the point spacing follows the planned speed, so it compresses under braking. Blue with markers, so the spacing is readable.
  • prediction (MPC output) — the horizon the controller solved for at this step (N+1 points). Time-varying; on a failed solve it is whatever the solver returned. Orange, switching to thick red while the solver status is non-zero.

target and prediction are (K, N+1, 2) arrays and the trace container's channel table is a whitelist of scalar / fixed-width rows, so they travel in a sidecar .npz beside the trace rather than inside the container:

run.vdtrace          # the trace, renderable on its own
run.qp.npz           # optional horizons sidecar, picked up automatically
key shape meaning
t (K,) step time [s] — matched to trace samples by nearest time, not by index
tgt_XY (K, N+1, 2) target horizon, world frame [m]
pred_XY (K, N+1, 2) prediction horizon, world frame [m]
status (K,) solver status, 0 = success
solve_ms (K,) solve time [ms] — optional, shown in the HUD
tgt_v (K, N+1) planned speed along the target [m/s] — optional
ego_XY (K, 2) rear-axle position per step — optional, used to fit the view

Only the first four keys are required, and the sidecar as a whole is optional: without one the renderer behaves exactly as it did before, and the two horizon artists are never created.

  • Write the horizons in the world frame. A controller solving in an ego or Frenet frame has to undo that transform first. The check is that pred_XY[k, 0] lands within ~0.1 m of the vehicle's rear-axle position at step k — if it does not, the frame is wrong.
  • Record a failed solve as it happened: keep that step's pred_XY, NaN included, instead of holding the previous one. The renderer skips non-finite points; overwriting them hides the failure the video is meant to show.
vdsim-render run.vdtrace --out run.gif --view-half 100 --preview-frame first-fail
  • --sidecar names the .npz explicitly. The default is the trace path with its suffix swapped for .qp.npz (run.vdtrace -> run.qp.npz) when that file exists, and no sidecar otherwise; passing the flag turns a missing file into an error instead of a silent skip.
  • --view-half sets the bird's-eye half-window [m]. The default window is derived from the wheelbase (~6 wheelbases) and is far shorter than an MPC horizon, so a 100 m horizon leaves the frame unless the window is widened.
  • --preview-frame util-peak (default, unchanged) puts the preview PNG at the friction-utilization peak; first-fail puts it at the first non-zero solver status, printing a note and falling back to the peak when the run never failed.
  • A run containing any failed solve also gets <out>_firstfail.png written next to the GIF, whatever --preview-frame says, and the BEV carries a QP FAIL (status=N) banner for as long as the status is non-zero.
  • Failure times are marked on the command panel as vertical spans, merged over consecutive failing steps. They come from an event overlay named qp_fail when the producer attached one, and from the sidecar status array otherwise.

All three flags are single-run only — overlay mode ignores them.

Replay in 3D — vdsim_render3d

The bird's-eye view above is flat by construction. A run recorded at schema 0.3 also carries attitude, ride height, body accelerations and the road contacts, and python/vdsim_render3d.py replays that in three dimensions from the trace file alone — no simulator, no scenario, no re-run:

PYTHONPATH=python python3 -m vdsim_render3d run.vdtrace \
    --out results/replay --cameras quarter,side,chase \
    --stride 5 --fps 20 --rt1 force,accel,saturation,normal

End to end, record and replay in one script:

python3 examples/demo_replay3d.py --out results/replay3d

The 2D renderer is untouched: vdsim_render and vdsim_render3d are separate modules that share only the container reader, so an existing BEV render of the same trace and preset stays byte-identical.

Render tiers. RT0 is always on — body box, four steered wheels, the ground grid and the driven trajectory. RT1 is opt-in per layer through --rt1:

layer draws needs
force per-wheel contact-force arrows wheel_F
accel the CG acceleration vector a_body
saturation wheel colour by friction utilization wheel_F, wheel_mu
normal per-wheel road normals at the contact points wheel_road_normal
refpath the path2d overlay, dashed a path2d overlay

--rt1 all turns them all on. Suspension links, tyre deformation and contact pressure (RT2) are not drawn: the trace has no input for them, so drawing them would mean inventing one.

Vector lengths are fixed constants — --force-scale (N per drawn metre), --accel-scale (m/s² per drawn metre) and --normal-scale (drawn length of the unit normal), all printed on every frame. Nothing autoscales per frame: if the same arrow length meant different things at different times the video would be misleading in a way that is very hard to notice.

Cameras are a combination, not a list. A camera is mount × aim × offset × projection × follow_attitude; the six names are aliases over that:

alias mount aim projection note
bev3d vehicle vehicle ortho top-down, the 3D counterpart of the 2D BEV
side vehicle vehicle ortho for reading roll, pitch and ride height
quarter vehicle vehicle persp three-quarter, the default
chase vehicle vehicle persp behind the car
map_fixed world fixed_point persp whole trajectory in one frame
map_track world vehicle persp fixed viewpoint, follows the car

follow_attitude defaults to yaw. Roll and pitch are deliberately not followed: a camera that rolled with the body would keep the horizon level and make the attitude unreadable, which is the one thing a 3D replay is for.

One file per camera, one pass over the trace. --cameras quarter,side writes <run_id>__<preset>__<camera>.mp4 plus a preview PNG for each; there is no grid composite, because splitting the resolution makes the HUD unreadable. Loading, decimation and the utilization derivation happen once regardless of how many cameras are drawn, so the total is base + N × draw. Measured on a 30 s L3 run (600 frames, ailab-12): load+prep 0.19 s either way, draw 13.3 s for one camera and 41.3 s for three.

ffmpeg is used when it is on PATH and is never bundled. Without it the render still succeeds, writing a PNG sequence and printing the one-line command that assembles it.

Older traces degrade instead of lying. A 0.2 trace has no pose_zrp, so the player draws z = 0, roll = pitch = 0, prints the reason once and labels every frame. And when contact_scope is C0/C1 the core never read the road normal's x/y, so the normal layer carries a normal: display-only badge — a tilted road drawn over flat physics is exactly the frame that would otherwise pass review.

Trace schema 0.3 — what a 3D replay needs recorded

Recording is still opt-in and still costs nothing when off (measured +0.08 % of step() on the OFF path against the previous revision; the gate is 1 %). What 0.3 adds:

  • model_level (L1..L5) and contact_scope (C0/C1/C2) in the manifest, both required. The level is what lets a reader tell "this model has no roll" from "the roll was never recorded"; the scope declares how far the road normal actually reaches into the physics.
  • geometry gains mass_kg, cg_height_m, wheel_radius_m, wheel_width_m and body_lwh_m. A 3D renderer sizes the body box, the wheel cylinders and the CG vector from these — without them it would be guessing, and a guessed body box is a picture that is quietly wrong.
  • five channels: pose_zrp (z, roll, pitch), a_body (ax, ay, az), wheel_road_dz, wheel_road_normal, wheel_travel.

a_body comes straight from the dynamics. It is not the derivative of v_body: at dt = 1 ms that difference is mostly noise, and its magnitude would change with the decimation setting.

A level that does not have a quantity does not record the channel. An L2 (7-DOF) run has no ride model, so it writes no pose_zrp at all rather than a column of zeros — a reader must be able to tell absence from zero, and the writer refuses a channel the declared level cannot produce.

The plant picks its level at construction, and the control contract u = [delta_rad, Fx_total_N] is the same at every level:

from vdsim_plant import VDSimPlant

plant = VDSimPlant(config="generic_sedan.yaml", level="L3")   # 14-DOF ride model
plant.reset([0, 0, 0, 22.0, 0, 0])
plant.enable_trace("run.vdtrace", seed=0, run_id="demo")
...
plant.finalize_trace()

level is one of L1..L5; L5 is the free-3D model on its penalty contact (no hardpoints) and runs with the vdsim_realtime L5 solver settings. Any other level string is a ValueError here and in every core entry point (K and L0 are also accepted by the session builders). L5 linearization and L5 in VecSession are refused explicitly in 0.8.1.

0.1 and 0.2 traces stay readable. A 0.3 file that omits any of the required fields is an error, not a warning.

Trace schema 0.4 — whether hardpoints were attached

0.4 adds one required manifest field, kinematics_attached (bool). It is the value the suspension-hardpoint attach actually returned for that run, not a guess from which YAML files exist. The physics of L3 and L4 differ only when hardpoints are attached, so this field is what tells a reader whether an L3/L4 trace carries suspension kinematics.

  • A 0.4 trace without kinematics_attached is an error.
  • A 0.3 or older trace reads with the value unknown (None) and one warning. It is never read as false.
  • level="L4" with no hardpoints is refused when the session is built (ValueError), so no L4 trace can carry L3 physics. VDSimPlant has no hardpoint input and refuses level="L4" for the same reason. An Experiment config gets hardpoints with kinematics: {front: mp_front_sedan, rear: ta_rear_sedan}.

Trace schema 0.5 — which channels the model actually computed

0.5 records two channels at every level, rp_rate (rad/s, [n,2] = roll rate, pitch rate) and wheel_travel (m, [n,4]), and adds one required manifest field, channel_validity, saying per channel whether the model computed it.

  • Values are modeled or not_modeled@<level>; the level must equal the manifest model_level.
  • The source is IVehicleDynamics::models_channel(), a pure virtual every dynamics model declares in the core. It is not derived from the level number: L5 stroke exists only with l5_spatial_suspension.
  • L1 and L2 (and K/L0) model neither channel: both are recorded as exactly 0.0 and marked not_modeled@L1 / not_modeled@L2. A 0.0 there means "not modeled", not "flat ride"; readers and renderers label it "not modeled".
  • L3 and above read the rates from the integrated roll and pitch states (state.angular_velocity), never by numerically differentiating anything.
  • A 0.5 trace without channel_validity is an error, and the writer refuses non-zero data under a not_modeled channel.
  • A 0.4 or older trace reads with validity unknown and one warning. It is never read as modeled. The 0.1–0.4 fixtures are unchanged; golden_v0_5.vdtrace is the 0.5 fixture.

Campaigns — many runs from one declaration

A run is one simulation and one .vdtrace. A campaign is a set of runs declared in a single YAML file. The runner owns only that upper layer: it adds no field to the trace and calls the render CLI unchanged.

# campaign.yaml
name: mu_sweep
base: step_steer          # configs/experiments/<name>.yaml, or an inline scenario
seed: 20260922            # root seed; each run's seed derives from it
duration: 4.0             # optional override [s]
sweep:
  grid:                   # orthogonal product; `list:` names combinations instead
    mu: [0.9, 0.7, 0.5]
  repeat: 1               # repeat each combination; only the seed moves
vdsim-campaign run campaign.yaml --jobs 4 --render overview
vdsim-campaign run campaign.yaml --resume      # continue where it stopped
vdsim-campaign run campaign.yaml --dry         # print the expansion, run nothing
python3 tools/vdsim_batch.py run campaign.yaml # same runner, historical path

Axis keys are dotted paths into the scenario document (mu, vehicle.*, tire.* are applied after the preset resolves). Products:

campaigns/mu_sweep/
├── campaign.yaml      # the declaration as executed
├── index.jsonl        # run_id · axes · seed · status · param_hash · role · trace_path · started_at · wall_s
└── 000/run.vdtrace    # one trace per run; run_id is a zero-padded ordinal
import vdsim_campaign as vc
for row in vc.read_index("campaigns/mu_sweep"):
    print(row["run_id"], row["status"], row["axes"])

Rules worth knowing before you rely on them:

  • Deterministic. seed = derive(root_seed, run_index) — no wall clock, no pid. --jobs 4 produces the same channel bytes as --jobs 1.
  • Failures are isolated, not hidden. One run is one child process; a run that diverges or crashes costs one index line with status diverged / error / killed. There are no retries unless you ask for --retry N.
  • Resume verifies. --resume skips a run only when its status is ok, its trace still exists, and its param_hash matches the declaration. If the declaration changed, the resume is refused.
  • The index is a lookup table, not a result database. Metrics belong in a consumer script that reads the index.
  • A render failure is not a run failure: status stays ok and render_status carries the error.

Full schema, index keys and the migration notes for the superseded runners: BATCH_RUNNER.

Config — parts catalog & scenes (v0.3)

Vehicles are blueprints over configs/parts/ (chassis, tire, drivetrain, …). Runs are scenes (configs/scenes/*.yaml) with a fleet[] of blueprint + part overrides. See CATALOG_AND_PHYSICS for layout, GUI API, drivetrain inertia, and opt-in LuGre tire.

Aerodynamics (0.8.0). A vehicle file can list named aero elements (aero.elements: ClA, CdA, x, z, optional y), set air_density, or keep the four legacy keys with an optional aero_drag_height. What each level applies, the axle split and the known L5 limit are in Ch.26 — Aero elements; check aero balance numbers on L1–L3.

Control — real-time UDP runtime

vdsim_realtime is VDSim's real-time application: it runs the same core against a wall clock and exchanges fixed-format binary UDP packets — CMD (steer / throttle / brake) in, STATE (pose, velocities, Fz, slip, tire forces, measured sensors, …) out. This is the SIL / HIL / co-sim boundary; the GUI and any external controller are just clients of it.

build/bin/vdsim_realtime --scene=configs/scenes/two_vehicle_race.yaml \
    --cmd-port=7001 --state-port=7002 --rate=200

The wire format is the canonical VDS1 binary protocol (cosim/cosim_protocol.hpp; contract in docs/vdsim_bridge_interface_requirements.md). Python clients encode CMD / decode STATE through cosim/protocol.py to stay byte-compatible.

Conventions

SI units · body frame ISO 8855 RH (X forward, Y left, Z up) · world frame ENU · wheel index FL = 0, FR = 1, RL = 2, RR = 3.

License

Apache-2.0 (core / kinematics / tools / validation / FMI export). FMI 2.0 headers are BSD-2-Clause (Modelica Association). Third-party deps (Eigen / yaml-cpp / spdlog / GoogleTest) keep their own licenses; fetched via CMake FetchContent.

About

No description, website, or topics provided.

Resources

Contributing

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages