Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,25 @@ one level-id factory, L5 from Python). Not released yet.
`carla_integration/python/vdsim_carla_bridge.py`,
`docs/figures_src/plot_theory_figures.py`.

## [0.8.2]

### Added
- `VehicleParams.steer_input` (`roadwheel` | `rack_travel`). Empty is `roadwheel`.
- L5 hardpoint path can read `SteeringOutput.rack_travel` as physical +y [m].
Rack mode skips `kRackPerRad` and Ackerman; wheel progress comes from DAE toe only.

### `rack_travel` meaning by level

| Level | Channel meaning | Left turn |
|---|---|---|
| L2-L4 | Wheel angle times `steering_ratio` (dimensionless) | + |
| L5 `roadwheel` (default) | Channel is not the L5 steer command | n/a |
| L5 `rack_travel` | Physical +y displacement [m] | Geometry decides. Rear-of-axle rack: left turn is -y |

Product toe is `atan2(spin_x, spin_y)` with right-side `s=-1`.
On the T5 fixture (tie-rod x behind the wheel centre) product-toe + is ISO 8855 steer -.
VDSim does not flip rear-axle rack sign in code; that correction belongs to the builder.

## [0.8.0] - 2026-10-29

### Changed — BREAKING: the four lift keys now act on L5
Expand Down
15 changes: 15 additions & 0 deletions core/include/vdsim/params.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ struct VehicleParams {
double steering_ratio {15.0}; // wheel/driver
double max_steer_angle_wheel {0.5}; // [rad]
double ackerman_percent {0.0}; // 0 = parallel, 100 = perfect Ackerman
// Empty or "roadwheel" = L5 uses the wheel-angle command (bit-identical).
// "rack_travel" = L5 hardpoint path reads SteeringOutput.rack_travel as +y [m].
std::string steer_input;
// Dynamic-steering Rack EOM params (used only when ISteeringSystem outputs mode=Dynamic).
bool steering_dynamic {false}; // true → DynamicSteering (Rack EOM, accepts torque/rate)
double rack_mass {30.0}; // [kg] effective rack + reflected column/motor inertia
Expand Down Expand Up @@ -163,6 +166,18 @@ const char* torque_reaction_name(VehicleParams::TorqueReaction r);
VehicleParams::TorqueReaction resolved_torque_reaction(const VehicleParams& vp,
bool drive, bool front);

/** True when steer_input is the L5 rack-travel mode. Empty = roadwheel. */
bool steer_input_is_rack(const VehicleParams& vp);

/** Reject unknown steer_input strings. Empty, roadwheel, and rack_travel pass. */
void validate_steer_input(const VehicleParams& vp);

/** L5: validate_steer_input plus Dynamic+rack rejection. */
void validate_l5_steer_input(const VehicleParams& vp);

/** L2-L4: one warning per process if rack_travel was requested. */
void warn_if_steer_input_unused(const VehicleParams& vp);

// Axle roll stiffness [N m/rad] derived from the corner springs plus the ARB.
// Two springs at half-track produce K = (k_left + k_right) * (track/2)^2; the
// anti-roll bar adds its own roll-only rate. axle: 0 = front, 1 = rear.
Expand Down
18 changes: 14 additions & 4 deletions core/src/free_3d_dynamics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include <atomic>
#include <cmath>
#include <cstdlib>
#include <stdexcept>
#include <limits>
#include <memory>
#include <numeric>
Expand Down Expand Up @@ -139,6 +140,8 @@ class Free3DDynamics final : public IVehicleDynamics {
drivetrain_ = make_default_drivetrain(vp_, vp_.drive_deadtime_s);
brake_ = make_default_brake(vp_, vp_.brake_deadtime_s);
steering_ = make_default_steering(vp_, vp_.steer_deadtime_s);
validate_steer_input(vp_);
validate_l5_steer_input(vp_);
for (int axle = 0; axle < 2; ++axle) {
const bool front = (axle == 0);
drive_unsprung_[axle] = resolved_torque_reaction(vp_, true, front)
Expand Down Expand Up @@ -560,10 +563,11 @@ class Free3DDynamics final : public IVehicleDynamics {
Fz[i] = std::clamp(k_tire_i * pen + c_vert * vn, 0.0, Fz_cap);
}
ctx.Fz = Fz;
const double d = steering_->apply(ctx).roadwheel_angle;
const bool rack_mode = steer_input_is_rack(vp_);
const double d = rack_mode ? 0.0 : steering_->apply(ctx).roadwheel_angle;

double d_FL = d, d_FR = d;
if (std::abs(d) > 1e-6 && vp_.ackerman_percent > 1e-9) {
if (!rack_mode && std::abs(d) > 1e-6 && vp_.ackerman_percent > 1e-9) {
const double frac = std::clamp(vp_.ackerman_percent / 100.0, 0.0, 1.0);
const double half = Tw_f * 0.5;
const double sgn = (d >= 0.0) ? 1.0 : -1.0;
Expand Down Expand Up @@ -997,7 +1001,11 @@ class Free3DDynamics final : public IVehicleDynamics {
const DriverCmd dc{cmd.steer_angle_wheel, cmd.throttle, cmd.brake,
cmd.gear, cmd.handbrake};
SubsystemContext ctx{state_, dc, 0.0};
const double steer_rad = steering_->apply(ctx).roadwheel_angle;
const SteeringOutput so = steering_->apply(ctx);
const char* disable = std::getenv("VDSIM_DISABLE_L5_RACK_TRAVEL");
const bool rack_disabled = disable && disable[0] == '1';
const bool rack = steer_input_is_rack(vp_) && !rack_disabled;
const double steer_rad = so.roadwheel_angle;
const Mat3 R = state_.orientation.toRotationMatrix();
for (int i = 0; i < NUM_WHEELS; ++i) {
const bool front = (i == WHEEL_FL || i == WHEEL_FR);
Expand All @@ -1007,7 +1015,9 @@ class Free3DDynamics final : public IVehicleDynamics {
mb::PrescribedCornerMotion mot;
mot.travel_z = state_.susp_compression[i];
mot.travel_z_dot = state_.susp_velocity[i];
mot.steer_rack_dy = front ? steer_rad * kRackPerRad : 0.0;
mot.steer_rack_dy = front
? (rack ? so.rack_travel : steer_rad * kRackPerRad)
: 0.0;
if (right) mot.steer_rack_dy = -mot.steer_rack_dy;
mb::WheelLoad wl;
wl.force_world = R * tire_F_[i];
Expand Down
35 changes: 35 additions & 0 deletions core/src/params.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,37 @@ VehicleParams::TorqueReaction resolved_torque_reaction(const VehicleParams& vp,
: VehicleParams::TorqueReaction::Unsprung;
}

bool steer_input_is_rack(const VehicleParams& vp) {
return vp.steer_input == "rack_travel";
}

void validate_steer_input(const VehicleParams& vp) {
const auto& s = vp.steer_input;
if (s.empty() || s == "roadwheel" || s == "rack_travel") return;
throw std::invalid_argument(
"steer_input: unknown value '" + s +
"' (expected roadwheel or rack_travel)");
}

void validate_l5_steer_input(const VehicleParams& vp) {
validate_steer_input(vp);
if (steer_input_is_rack(vp) && vp.steering_dynamic) {
throw std::invalid_argument(
"steer_input=rack_travel is not supported with Dynamic steering");
}
}

void warn_if_steer_input_unused(const VehicleParams& vp) {
if (vp.steer_input.empty() || vp.steer_input == "roadwheel") return;
validate_steer_input(vp);
static std::once_flag once;
std::call_once(once, [&] {
spdlog::warn(
"[vdsim] steer_input='{}' is ignored on L2-L4; L5 hardpoint path only",
vp.steer_input);
});
}

// =============================================================================
// VehicleParams
// =============================================================================
Expand Down Expand Up @@ -299,6 +330,8 @@ VehicleParams VehicleParams::from_yaml(const std::string& path) {
pull(root, "steering_ratio", p.steering_ratio);
pull(root, "max_steer_angle_wheel", p.max_steer_angle_wheel);
pull(root, "ackerman_percent", p.ackerman_percent);
pull(root, "steer_input", p.steer_input);
validate_steer_input(p);

pull(root, "aero_drag_coeff", p.aero_drag_coeff);
pull(root, "frontal_area", p.frontal_area);
Expand Down Expand Up @@ -376,6 +409,8 @@ void VehicleParams::to_yaml(const std::string& path) const {
out << YAML::Key << "steering_ratio" << YAML::Value << steering_ratio;
out << YAML::Key << "max_steer_angle_wheel" << YAML::Value << max_steer_angle_wheel;
out << YAML::Key << "ackerman_percent" << YAML::Value << ackerman_percent;
if (!steer_input.empty())
out << YAML::Key << "steer_input" << YAML::Value << steer_input;

if (aero_elements.empty()) {
out << YAML::Key << "aero_drag_coeff" << YAML::Value << aero_drag_coeff;
Expand Down
2 changes: 2 additions & 0 deletions core/src/seven_dof_dynamics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ class SevenDOFDynamics final : public IVehicleDynamics {
vp.mass, vp.wheelbase, vp.track_front,
static_cast<int>(vp.differential), vp.ackerman_percent,
vp.brake_ebd_enabled);
validate_steer_input(vp);
warn_if_steer_input_unused(vp);
}

// R8: the per-wheel tire relaxation (belt) transients are the model state
Expand Down
8 changes: 8 additions & 0 deletions core/src/sim_session.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,14 @@ ControlInput with_steer_angle(const ControlInput& u, double steer) {
void SimSession::tick(double dt) {
if (!(dt > 0.0)) return;

// T3: rack_travel on L5 requires front hardpoints. Must throw here
// (not in IVehicleDynamics::step, which is noexcept).
if (steer_input_is_rack(vp_) && dyn_->level() == IVehicleDynamics::Level::L5_Stunt &&
!free_3d_mb_dynamics_enabled(*dyn_, 0)) {
throw std::invalid_argument(
"steer_input=rack_travel requires front suspension hardpoints on L5");
}

ControlInput cmd;
State s;
{
Expand Down
7 changes: 7 additions & 0 deletions cosim/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,13 @@ if(Python3_FOUND)
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
TIMEOUT 300
ENVIRONMENT "VDSIM_BUILD_DIR=${CMAKE_BINARY_DIR};PYTHONPATH=${CMAKE_BINARY_DIR}/python:${CMAKE_SOURCE_DIR}/python")
add_test(NAME l5_rack_travel
COMMAND ${Python3_EXECUTABLE}
${CMAKE_SOURCE_DIR}/tests/scripts/test_l5_rack_travel.py)
set_tests_properties(l5_rack_travel PROPERTIES
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
TIMEOUT 300
ENVIRONMENT "VDSIM_BUILD_DIR=${CMAKE_BINARY_DIR};PYTHONPATH=${CMAKE_BINARY_DIR}/python:${CMAKE_SOURCE_DIR}/python")
# Level ids at every Python entry point (work order 44): accepted set,
# ValueError for unknown ids, L5 sessions bit-identical to the C++ model
# built directly, L5 finiteness and solver record, realtime refusal.
Expand Down
4 changes: 2 additions & 2 deletions docs/VALIDATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,11 @@ version and the new numbers have to land in the same commit.

<!-- VALIDATION-CURRENCY BEGIN -->
```text
tests: 587/587
tests: 595/595
config: cmake --preset validation && cmake --build --preset validation && ctest --preset validation
presets: CMakePresets.json@c98fe5e64c842faaeb06598af46476f3507c18a6
toolchain: cmake 3.31.10
commit: 69d5da46f6aac8e4a937747c91d42e9caf145833
commit: c63aa1fbcabd294aa1680d3a865cee2b8d402ae2
date: 2026-11-02
excluded: gui_v3_e2e (GUI v3 test group formally deferred -- VDSIM_BUILD_GUI_V3_TESTS=OFF)
excluded: gui_v3_api_smoke (GUI v3 test group formally deferred -- VDSIM_BUILD_GUI_V3_TESTS=OFF)
Expand Down
32 changes: 32 additions & 0 deletions docs/theory/14_hardpoint_kinematics.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,3 +319,35 @@ lookup 은 고정 hardpoint 의 사전 sweep. hardpoint 가 runtime 에 변하
> `diagnose.py` 출력 예: DW RC 7.5cm/camber −0.094°/mm/wheel-z err <0.1μm,
> MP strut 19.4°, TA semi-trailing 14°/anti-dive 5.5°, 5-link lengths
> [0.36,0.36,0.38,0.38,0.35].


## 14.16 L5 `steer_input` and rack travel

`VehicleParams.steer_input` selects how L5 hardpoint corners read the steer command.
Empty and `roadwheel` keep the 0.8.1 path: wheel angle times `kRackPerRad`.
`rack_travel` copies `SteeringOutput.rack_travel` as physical +y [m] into `steer_rack_dy`
(front only). The S2 right-side invert stays. `kRackPerRad` and Ackerman are not used.
`d_wheel` is `{0,0,0,0}`; the progress angle is DAE product toe (`toe_dae_`).

Product toe = `atan2(spin_x, spin_y)` with right-side `s=-1`
(`multibody_hard_dae.cpp`). On this product, left-wheel product-toe + is ISO 8855 steer -.
Rear-of-axle rack sign correction is the builder's job, not a VDSim code flip.
Hardpoints missing, Dynamic steering, or a bad key raise `ValueError`.
L2-L4 ignore the key (one process warning if it is set).


정지 기하 수용 범위는 적재 차량의 첫 공통 유한 시간 준정적 구간임.
영·양·음 랙을 1 ms 간격으로 동기화하고 첫 0.2 s 영 입력 뒤 랙 적용함.
최대 8 s 안에 모든 실행이 아래 조건을 동시에 0.5 s 연속 만족한 첫 구간만 판정에 사용함.

| 감시량 | 연속 구간 조건 |
|---|---|
| 차체 속도 노름 | ≤ 0.001 m/s |
| 차체 각속도 노름 | ≤ 0.001 rad/s |
| 바퀴별 이동률 절댓값 | ≤ 0.001 m/s |
| 제품 토 변화율 절댓값 | ≤ 0.01 deg/s |
| 바퀴별 수직 하중 변화율 절댓값 | ≤ 1 N/s |
| 감시 채널·수직 하중 | 전부 유한·모든 바퀴 하중 양수 |

동일 시각의 영 랙 대비 제품 토 차분을 고정 기하표와 비교함.
기한 내 공통 정착 부재는 실패이며 8 s 이후 영구 평형 보장은 범위 밖임.
1 change: 1 addition & 0 deletions python/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,7 @@ PYBIND11_MODULE(vdsim, m) {
.def_readwrite("steering_ratio", &vdsim::VehicleParams::steering_ratio)
.def_readwrite("max_steer_angle_wheel",&vdsim::VehicleParams::max_steer_angle_wheel)
.def_readwrite("ackerman_percent", &vdsim::VehicleParams::ackerman_percent)
.def_readwrite("steer_input", &vdsim::VehicleParams::steer_input)
.def_readwrite("steering_dynamic", &vdsim::VehicleParams::steering_dynamic)
.def_readwrite("rack_mass", &vdsim::VehicleParams::rack_mass)
.def_readwrite("rack_damping", &vdsim::VehicleParams::rack_damping)
Expand Down
1 change: 1 addition & 0 deletions tests/integration/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ add_executable(vdsim_integration_tests
test_l5_driving.cpp
test_l5_strut_validation.cpp
test_l5_reaction.cpp
test_l5_rack_travel.cpp
test_terrain_l5.cpp
test_belt_transient.cpp
test_belt_validation.cpp
Expand Down
Loading
Loading