Repository navigation
Feat/poc w5 to 95pct - #1
Merged
Merged
Conversation
- Combined slip via friction-ellipse rescale: (Fx/Dx)^2 + (Fy/Dy)^2 <= 1. - Self-aligning Mz = -t_p(alpha) * Fy with pneumatic-trail falloff. - Camber thrust API: Fy_camber = -C_gamma * gamma * Fz * mu_lat (default off). - Pure-slip cases unchanged (backward-compat with prior 8 tire tests). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… / 8 scenarios
VehicleParams (extended):
- Differential (Open/Locked/LSD) + lsd_preload + lsd_ramp
- Ackerman percent + brake_bias_front + brake_ebd_enabled
- Aero downforce (Cl_front, Cl_rear) + anti_dive_front + anti_squat_rear
- camber_per_roll (roll-to-camber gain, currently API only)
TireParams (extended):
- combined_slip_enabled, pneumatic_trail, trail_falloff_alpha
- camber_stiffness, tire_vertical_stiffness
SolverParams: from_yaml/to_yaml + integrator string + dt/substeps validation.
Scenario (new):
- name / initial_vx / duration / dt / mu + ZOH/linear control profile
- Time-varying mu_profile (icy patch / wet transition)
- ControlSample (t, throttle, brake, steer, gear) + sample(t)
configs/:
- vehicles: sedan / sports / fsk_formula / race_car
- tires: default_pacejka
- solvers: rk4_1ms / euler_10ms
- scenarios: step_steer / double_lane_change / throttle_brake_sequence /
ice_patch / j_turn / skidpad / brake_in_turn / ice_corner
- sweeps: aero_vs_vx
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Quasi-static longitudinal weight transfer (1-step lag on ax to avoid self-reference): dFz_long = m * ax_prev * h_cg / L. - Aerodynamic downforce per-axle: Fz_aero = Cl * 0.5*rho*A * vx*|vx|. - Rolling resistance: f_rr * Fz_total * tanh(vx/0.5), default off. - Brake bias_front + EBD (dynamic Fz-based bias). - Body-frame Mz aggregation: Mz_total += F_f.Mz + F_r.Mz. - Lc1-PerWheel / Lc2-AxleTorque / Lc3-FxTotal -> Lc4-Pedal lowering via std::visit (lower_to_l4 helper). - NaN/Inf input guard: sanitize CmdL4 throttle/brake/steer + dt. - spdlog::debug on initialize(). Tests: - test_accel_brake: 3 LongScenarios + RR-vs-no-RR comparison. - test_weight_transfer: 6 tests including bias/EBD influence. - test_step_steer_sweep: 3x5 grid linear-region |err| <= 10%. - test_control_dispatch: variant CmdL1/L2/L3 dispatch + NaN sanitize. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…steering rack torque - Per-tire Fz with longitudinal + lateral weight transfer (1-step lag). - Roll-stiffness ratio splits lateral transfer between front/rear axles. - 4 independent wheel-spin DOFs. - Differential model: Open / Locked (smooth tanh bias) / LSD (preload + ramp). - Ackerman steering (0-100%): inside/outside wheel angle per turn geometry. - Aerodynamic downforce additive to static Fz. - Brake bias + EBD (dynamic Fz fractioning). - Body-frame Mz aggregation across 4 tires. - ax_body / ay_body diagnostics (Fx_total/m, Fy_total/m) + steering rack torque (front-axle Mz sum * steering_ratio). - spdlog::debug on initialize(). - Lc1-Lc4 dispatch via lower_to_l4. Tests (test_seven_dof.cpp): - ConstructionAndLevel, AtRestStaticFz - HardBrakeLoadsBothFrontWheels, LeftTurnLoadsRightWheels - SmallSteerYawRateMatchesBicycle, ZeroSteerStraightLine - AeroDownforceIncreasesFzAtSpeed, AeroDownforceZeroAtRest - OpenDifferentialSplitMuSpins, LockedDifferentialReducesSpread, LSDBetweenOpenAndLocked - RollAngleSignAndScale, PitchAngleSignDuringBrake - BicycleDefaultDiagnosticsRollPitchZero - AckermanInfluencesTurningRadius, AckermanZeroReproducesBaseline - SteeringRackTorqueSignOpposesSteer - IndependentWheelSpinUnderSplitMu Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…+ anti-dive
- Planar motion delegated to Ld2-SevenDOF inner solver.
- 3 sprung vertical DOF (heave z_s, roll phi, pitch theta) with RK4.
* Heave + pitch via per-corner spring/damper between sprung corner
and unsprung mass.
* Roll uses axle-level roll_stiffness (springs + ARB combined).
* Anti-dive (front, brake) / anti-squat (rear, accel) scales inertial
pitch moment.
- 4 unsprung mass DOF (z_u_i) with k_tire vertical stiffness.
* m_u * z_u_ddot = -F_susp_on_sprung - k_tire * z_u
- Pose orientation = quat(roll, pitch, yaw).
- susp_compression / susp_velocity diagnostics updated from sprung-corner
relative motion.
- Lc1-Lc4 dispatch + NaN guards.
- spdlog::debug on initialize().
- dynamics_stubs.cpp: L3 stub removed, only fourteen_dof retained as factory.
Tests (test_fourteen_dof.cpp):
- ConstructionAndLevel, StaticSuspensionPopulatedAtRest
- CompressionGrowsUnderBrakeOnFront, PlanarMotionMatchesL2Closely
- PoseEncodesRollAndPitch, RollOscillatesAndSettles
- PitchTransientUnderBrake, AntiDiveReducesPitchUnderBrake
- SuspensionVelocityNonZeroDuringTransient
test_bicycle_steady_state.cpp: BicycleStubs assertion changed to
AllLevelsConstructible (all factories now valid).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…aypoint + Driver model IVehicleDynamics interface (extension): - gamma (camber) added to ITireModel::Input. - Virtual default getters: roll_angle_qs, pitch_angle_qs, ax_body_est, ay_body_est, steering_rack_torque (Ld1 returns 0). ControlConverter (new): - LongAxController (Lc5): ax_target -> throttle/brake, PI + feed-forward with anti-windup, mutually-exclusive throttle/brake output. - LongVxController (Lc6): v_target -> ax_target cascade PI. - PurePursuitController (Lc7): waypoints + lookahead -> steer. - DriverModel (Lc7+latency+noise): wraps PP + Lc6 + Lc5; ring-buffer reaction-time delay; Gaussian noise via Box-Muller. Tests: - test_control_converter.cpp: 7 LongAx tests (zero/sign/clamp/wind-up/reset). - test_control_l6_l7.cpp: 4 LongVx + 3 PurePursuit + 2 DriverModel tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- carla_integration/plugin/raycast_contact_provider.{hpp,cpp}
* IContactProvider impl backed by injectable RaycastFn callback.
* Surface ID -> (mu_long, mu_lat) lookup table for material switching.
* 4-wheel per-tick raycast from wheel world position downward.
* Penetration estimate vs wheel_radius_nominal.
- Static lib vdsim_carla_plugin (VDSIM_BUILD_CARLA_PLUGIN=ON).
- Header-only, no CARLA/UE5 dependency at build time; ABI ready for
UE5 host process to plug its raycast callback at runtime.
Tests (test_carla_plugin.cpp):
- FlatGroundLookupYieldsKnownMu
- UnknownSurfaceFallsBackToDefault
- MissedRaycastInvalidatesContact
- NullRaycastSafe
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ters - python/bindings.cpp: VehicleParams / TireParams / SolverParams / State / ContactPoint / CmdL4 / IVehicleDynamics + factories + LongAxController / LongVxController. Enums: Drive / Differential / Integrator / Level. - python/CMakeLists.txt: pybind11 auto-detected via Python -m pybind11 --cmakedir fallback. Module output as build/python/vdsim.so. VDSIM_BUILD_PYTHON=ON option. - python/sweep_runner.py: Cartesian product parameter sweep driving any binary (vdsim_l1_vs_l2 / vdsim_scenario_run); per-cell YAML mutation. - python/csv_to_scenario.py: ADMA / CarMaker telemetry CSV -> scenario.yaml. - python/tir_to_yaml.py: AVL .tir file -> TireParams YAML (12 fields). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Header-only skeleton for the post-graduation multibody track (hardpoint-driven suspension synthesis on top of the same C++17 / pybind11 stack). Data structures (vdsim::mb namespace): - RigidBody: mass + inertia diagonal + CG offset + Newton-Euler state. - Joint: Ball / Revolute / Cylindrical / Prismatic / Universal / Rigid. - Bushing: 6-DOF linear stiffness + damping in local frame. - Hardpoint: named coordinate in a body's frame. - SuspensionTopology: bodies + joints + bushings + hardpoints + K&C diagnostic outputs (toe / camber / caster / kingpin / scrub / trail). - WheelLoad / WheelPose IO structs. - IMultibodySolver virtual interface (forward_kinematics / quasi_static_compliance / step_dynamics) -- impl reserved for M1+. Suspension topology YAML schemas (already in commit 2 under configs/suspensions/): MacPherson, double-wishbone, multi-link-5, trailing-arm, beam-axle, twist-beam. Tests: BasicTypesInstantiate, SuspensionTopologyDiagOutputsDefaultZero, AllTopologyKindsEnumerable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- vdsim_bicycle_run: single-tier Lc4-Pedal scenarios on Ld1-Bicycle. - vdsim_scenario_run: Scenario YAML -> 14-column time-series CSV with time-varying mu support. - vdsim_l1_vs_l2: emits Ld1/Ld2 paired CSVs for the same scenario. - vdsim_ax_track_demo: Lc5-AxTarget PID step profile on Ld2. - vdsim_path_tracking: Lc4 <- Lc5 <- Lc6 <- Lc7 cascade on figure-eight path. - vdsim_driver_demo: DriverModel (latency + Gaussian noise) on Ld2. - vdsim_l3_demo: Ld3-FourteenDOF transient trace with susp_velocity dump. - vdsim_split_mu_demo: split-mu accel under Open / Locked / LSD diffs. - vdsim_l1_vs_l2 (paired): identical scenario, two dyn levels side by side. - examples/CMakeLists.txt registers all binaries, output to build/bin/. VDSIM_BUILD_EXAMPLES=ON default. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…EADME quick-tour - CMakeLists.txt: VDSIM_BUILD_EXAMPLES / VDSIM_BUILD_PYTHON / VDSIM_BUILD_CARLA_PLUGIN options (examples + python + carla now ON-able). examples/, python/, carla_integration/ subdirs. - third_party/CMakeLists.txt: CMAKE_POSITION_INDEPENDENT_CODE=ON so spdlog / yaml-cpp static libs can link into pybind11 shared module. - core/CMakeLists.txt: bicycle / seven_dof / fourteen_dof / params / scenario / control_converter sources added. - tests/unit/CMakeLists.txt: 9 unit-test files registered + vdsim_carla_plugin link. - tests/integration/CMakeLists.txt: 6 integration files. - README.md: updated quick-tour with 10 example binaries, Python module build, scenario YAML usage, PoC status table. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tive matrix Per-cycle task reports (docs/tasks/NN_*/README.md), each with: - 목적 / 구현 방법 / 검증 방법 (근거) / 검증 결과 (표 + figure) / 판단. Coverage: - W4 Ld1-Bicycle finalize (11-14) - Tire combined slip + Mz (15) / Tire/Solver YAML I/O (16) - Ld1 weight transfer (17), Scenario YAML DSL (18), Ld2-SevenDOF (19) - L1/L2 grid sweep (20), Ackerman (21), roll/pitch (22), Mz aggregation (23) - Ld3 skeleton (24), L5 ControlConverter (25), Differential (26), Aero downforce (27), Sweep DSL (28), Brake bias (29) - Ld3 dynamic 3 DOF (30), L6/L7/L8 cascade (31-33) - Anti-dive + camber (36-37), Extra configs + time-varying mu (38-39) - PoC summary v1 80% (40) - Ld3 full 14-DOF + CARLA + dispatch (41-43) - EBD + Pybind11 + spdlog (44-46) - MF96 validation + CSV importer (48-49) - Benchmark matrix + L3 ride freq (50-51) - PoC summary v2 92% (52) - Driver + .tir + scenarios + RR (53-56) - Driver demo + skidpad + brake-in-turn (57-59) - NaN guards + README + cycles overview (60-62) - Understeer + brake distance + L2/L3 logging (63-65) - Steering rack torque + ice corner (66-67) - PoC summary v3 95% (68) - Competitive matrix + multibody M0 + suspension schemas (69) docs/figures_src/: 20+ matplotlib scripts generating all task PNGs. Index in docs/README.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…altime - viewer/index.html: single-page Three.js viewer (z-up ISO 8855 RH). * Vehicle box + 4 cylinder wheels + yellow nose marker. * Body roll/pitch applied to chassis/cabin. * Front-wheel steering visualization. * Wheel-spin animation from vx / wheel_radius. * Trail polyline (8000 pts max, clearable). * Camera modes: Orbit / Follow / Chase-close / Top-down. * CSV upload + parse + binary-search interp. * Timeline slider + play/pause/rewind + 0.25x..10x speed. * HUD: t / vx / vy / r / ax / ay / roll / pitch / steer / throttle / brake. * WebSocket client (auto-prefers realtime queue over CSV replay). - viewer/realtime_server.py: asyncio + websockets bridge. * Drives the vdsim Python module live at user-specified FPS. * Default 30 FPS with 0.005 s inner Ld step (6 substeps per frame). * Built-in Pure Pursuit + Lc6 PI driver for figure-eight demo. * --vehicle / --tire / --level (L1/L2/L3) / --driver / --v_target / --fps. - viewer/README.md: quick start, controls, CSV format, known limits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Background reference objects added so motion is visible at a glance: - Dashed yellow figure-8 path on the ground (R=20 m, 160 segments). - 40 orange traffic cones + white bases along the path (every ~5 m). - Two red 4 m posts with yellow caps at the figure-8 centers. - Radial distance markers (20 / 40 / 60 / 80 m, 4 azimuths). No realtime_server change; pair with --v_target 13 to see cornering. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…trol ladder)
Self-contained theoretical reference for the VDSim implementation, written
at PhD-student lecture-note depth. Each chapter pairs equations + physical
intuition + VDSim file:line mapping + verification test + textbook citations.
Chapters:
- README.md index + reading order + reference list + notation
- 01_frames ISO 8855 RH vs SAE J670, quaternions, slip definitions
- 02_rigid_body Newton-Euler in body frame, Coriolis cross-terms,
kinematic ax/ay vs frame-velocity derivatives
- 03_tire_pacejka MF96 sin(C·atan(...)) derivation, friction-ellipse
rescale, Mz pneumatic-trail falloff, camber thrust
- 04_ld1_bicycle 5-DOF EoM, longitudinal weight transfer (1-step lag),
analytical SS yaw rate derivation, understeer gradient
- 05_ld2_seven_dof per-tire Fz with lateral transfer (roll stiffness
share), Ackerman geometry (% interpolation), Open /
Locked / LSD differential models, steering rack torque
- 06_ld3_fourteen_dof sprung 3 DOF (heave/roll/pitch) + unsprung 4 DOF,
anti-dive geometry, RK4 14-DOF integration, wheel-hop
frequency analysis (and overdamped limit)
- 07_control_ladder Lc1-Lc8 strawman, variant + visit + if-constexpr
dispatch, lower_to_l4 lowering, m × n matrix claim
Chapters 08-13 (PID controllers, Pure Pursuit, Driver model, RK4,
software architecture, multibody Ld4-Ld5 outlook) reserved for next turn.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Completes the textbook-grade theoretical reference. Same depth as 01-07:
each chapter pairs equations + physical intuition + VDSim file:line +
verification + textbook citations.
- 08_pid_controllers Lc5-AxTarget PI + feed-forward, anti-windup,
throttle/brake auto-branch, Lc6-VTarget cascade,
inner/outer bandwidth separation
- 09_pure_pursuit_path Lc7-PathCurvature geometry derivation (R_path =
Ld² / (2·dy_b)), vx-dependent Ld scheduling,
Stanley comparison, Lc8 Frenet outlook
- 10_driver_model Reaction-time ring buffer, Box-Muller Gaussian
noise, autonomous-vs-human-driver comparison
- 11_numerical_integration Euler vs RK4 error orders + stability regions,
substepping + 1 ms inner dt rationale,
1-step lag derivation and bias analysis
- 12_software_architecture Factory + pure-virtual ABI stability, std::variant
+ visit + if-constexpr dispatch, PIC pitfall for
pybind11, CARLA plugin raycast injection pattern
- 13_multibody_outlook Lumped Ld3 vs full multibody Ld4-Ld5, hardpoint
+ joint + bushing schema, MacPherson FK sketch,
Featherstone outlook, M1-M7 roadmap, Adams Car
cross-validation strategy
README.md updated: 01-13 all marked draft-1-complete.
Total: ~ 5000 lines markdown across 14 files. Self-contained theoretical
backing for the PoC implementation suitable for paper introduction,
lecture material, or sales / PT use.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s CI
- mkdocs.yml: Material theme, palette toggle (light/dark), instant search,
navigation sections + tabs, copyable code blocks, MathJax (arithmatex),
mermaid via pymdownx.superfences custom_fences, mike version provider
- docs/index.md: home page with Ld × Lc m × n mermaid + status + 2-ladder table
- docs/tasks/README.md: PoC report index (highlight + all 60+ reports)
- docs/examples/README.md: CLI binaries + Python pybind11 + sweep_runner
- docs/theory/07_control_ladder.md: cascade ASCII -> mermaid flowchart
- docs/theory/12_software_architecture.md: 4-layer ASCII -> mermaid
GitHub Actions:
- .github/workflows/docs.yml : on push to main, mike deploy "main"
with latest alias (auto-publish).
- .github/workflows/docs-release.yml : on tag v*.*, mike deploy version
with "stable" alias; manual dispatch
supported.
Local: `mike deploy --update-aliases v0.95-PoC latest` then `mike serve`
publishes the gh-pages branch ready for GitHub Pages.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pipeline (per tick):
1. query 4-wheel contacts from CARLA via world.cast_ray
2. run VDSim dynamics.step() (Ld1 / Ld2 / Ld3, --level)
3. ISO 8855 RH → CARLA / UE4 frame conversion (y, yaw sign flip)
4. actor.set_transform() + set_target_velocity / angular_velocity
5. world.tick() in synchronous mode (50 Hz default)
CARLA's own physics is disabled via set_simulate_physics(False) so VDSim
is the sole authority for ego motion; CARLA still drives rendering,
sensors, NPCs, traffic manager.
Files:
- vdsim_carla_bridge.py : BridgeConfig + VDSimCarlaBridge class
- run_demo.py : --driver Pure Pursuit on figure-8 + Lc6 PI
(or square steer demo if --driver omitted)
- test_bridge_offline.py : 5 tests — frame conv, wheel offsets, mocked
bridge construction + step + telemetry
(works without CARLA server)
- README.md : architecture diagram + quick start + frame
conventions + limits
Default tier: Ld2-SevenDOF; L1/L2/L3 selectable via --level.
CARLA 0.9.15/0.9.16 client API tested.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- run_demo.py: tick world after spawn before reading actor pose (sync mode) - record_demo.py: RGB camera + per-tick telemetry CSV + safe cleanup - results/run02: 15s figure-8 demo, 750 samples, 3.8 MB MP4, plot - T22 report: physics validation (lateral load transfer, frame conv, TPS) - .gitignore: site/, raw PNG frame dirs Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
T23 (sim_14dof_jump.py): 2D side-view jump on half-cosine ramp. - Extends Ld3 with world-z, gravity, ground profile input, airborne branch - Droop/bump stop clipping to bound spring force during free-fall - matplotlib animation + 4-panel telemetry plot T24 (sim_3d_jump.py): 3D turning jump with full vertical+lateral coupling. - Planar (vy, yaw, roll) + Pacejka MF96 (combined slip + load sensitivity) - Relaxation length transient α_dyn (1st-order lag, σ_y / |v|) - Per-wheel slip + wheel spin (4-DOF) + drive/brake torque + RWD - Contact-gated inertial coupling moment using h_cg - h_roll/pitch axis - 3D matplotlib animation (Poly3DCollection box + wheels + ramp) sports.yaml: add roll_center_height_front/rear + pitch_center_height for proper moment arm formulation (instead of ground-referenced h_cg). docs/deck_src: PoC one-pager (PNG/PDF/PPTX) for sales/PT use. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pacejka MF96 (core/src/pacejka_mf96.cpp):
μ_eff(Fz) = μ_nominal · (1 - load_sensitivity · (Fz/Fz_nominal - 1))
Floor at 0.3 · μ to keep numerics sane at extreme Fz.
Ld2 seven_dof (core/src/seven_dof_dynamics.cpp):
- Per-wheel transient slip α_dyn[4] state, updated between substeps
via exact exponential: α_dyn(t+h) = α_geom + (α_dyn − α_geom)·exp(−v·h/σ)
- tire input gamma from external camber (set_camber_per_wheel) instead of 0
Ld3 fourteen_dof (core/src/fourteen_dof_dynamics.cpp):
- Each step, compute per-wheel camber = ±camber_per_roll · phi_
(left/right opposite signs) and forward to inner Ld2 via the new
IVehicleDynamics setter — closes the roll → camber → tire Fy_camber path.
IVehicleDynamics interface (core/include/vdsim/interfaces.hpp):
+ set_camber_per_wheel(array<4>) noexcept — default no-op, Ld2 overrides
Python bindings (python/bindings.cpp):
+ ITireModel, TireInput, TireOutput, create_pacejka_mf96, create_linear_tire
+ dyn.set_camber_per_wheel, tire_forces_body, wheel_slip_ratio/angle
+ TireParams: load_sensitivity, relaxation_length_lat/long, trail_falloff_alpha
Tests:
+ 3 unit (load sensitivity no-op / fade / floor clamp)
+ 1 integration (Ld2 relaxation length delays Fy build-up)
+ 1 integration (Ld3 roll → camber → tire — sign + presence)
All 149/149 tests pass (was 144).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bicycle_dynamics.cpp:
- Per-axle transient slip α_dyn_f/r (front/rear) with exponential update
between substeps; matches the Ld2 implementation.
- Geometric α and wheel Vx cached during derivatives() for the substep-end
relaxation step.
pacejka_mf96.cpp:
- Camber now contributes to Mz:
Mz_camber = -pneumatic_trail · 0.25 · camber_stiffness · γ · Fz · μ
(linear approximation; full MF would have its own coefficient).
Tests:
+ Bicycle.RelaxationLengthDelaysLateralForce (mirrors Ld2 test).
+ PacejkaMF96Fixture.CamberContributesToMz — anti-symmetric in γ.
151/151 tests pass (was 149).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 2 Ld4-MultibodyKinematic foundation:
- YAML hardpoint schema (LCA + UCA + tie rod + spring/damper).
- 2D analyzer (tools/kinematics/double_wishbone.py):
* Instant center (LCA-line ∩ UCA-line)
* Roll-center height via tire-contact → IC extrapolation
* Linear camber gain (IC method)
* Closed-form 2-bar linkage sweep over wheel travel ±80 mm
* Track change vs travel
- Sports-trim sample config gives RC = 7.5 cm, camber gain -0.092 °/mm
at static (matches sports-car textbook range).
- Linear vs nonlinear cross-check: 7% agreement at z=0.
Outputs: side_view.png + travel_sweep.png + sweep CSV (Stage C lookup table
input) + summary.yaml + T27 report with limits and next-stage plan.
This is the first step in replacing Ld3's lumped camber_per_roll / h_rc
parameters with geometry-derived values.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stage B — tools/kinematics/dw_3d_solver.py:
- Full 3D linkage solve via revolute LCA/UCA axes + trilateration for the
tie-rod knuckle attach (3-sphere intersection: LK, UK, TR_inner).
- Output per (wheel_travel, steer_rack_dy): camber, toe, track_change,
caster. CSV sweep + 4-panel plot.
- Static check passes: camber/toe/track = 0 at (0,0), caster = +1.43°
matches kingpin x-tilt from sample hardpoints.
Stage C — core/include/vdsim/suspension.hpp + suspension_lookup.cpp:
- ISuspensionKinematics interface: compute(travel, steer) → 4 outputs.
- create_lookup_kinematics(csv): loads dw_3d_solver.py output as bilinear
interpolation table; clamps outside grid.
- 5 unit tests (exact grid / interp / clamp / linear track).
Stage D — core/src/fourteen_dof_dynamics.cpp:
- Holds optional unique_ptr<ISuspensionKinematics> for front and rear.
- Each step: per-wheel travel = z_u - z_corner_sprung (signed for bump>0).
If kinematics attached → query lookup, forward camber to inner L2.
Otherwise → legacy phenomenological vp_.camber_per_roll · phi (back-compat).
- Free functions vdsim::attach_front_kinematics / attach_rear_kinematics
downcast to FourteenDOFDynamics and install the lookup.
L3 contract: AttachedKinematicsOverridesCamberPerRoll integration test ─
camber_per_roll=0 + attached 5° lookup → front Fy shifts > 100 N from
baseline. Confirms the path is live.
All 157/157 tests pass (was 151).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stage G — toe-per-wheel propagation through L3 → L2:
- IVehicleDynamics::set_toe_per_wheel(array<4>) added (default no-op).
- SevenDOFDynamics: stores toe_ext_[4], adds to each wheel's steer angle
(front: Ackerman + toe_ext, rear: toe_ext alone — was previously always 0).
All wheels now project body force ↔ wheel frame uniformly.
- FourteenDOFDynamics: in step(), queries kinematics at (travel, 0) for each
wheel; forwards both camber AND toe to inner L2. Sign-flip on right side.
- New test FourteenDOF.KinematicsToeProducesBumpSteer: with zero driver
steering input + a constant 0.05 rad lookup toe → front wheels develop
measurable slip angle (≥ ~1°) and Fy from the kinematic toe alone.
Stage I — Python bindings for ISuspensionKinematics:
- vdsim.KinematicsOutput (camber/toe/track_change/caster)
- vdsim.ISuspensionKinematics.compute(wheel_travel, steer_input)
- vdsim.create_lookup_kinematics(csv_path)
- vdsim.attach_front_kinematics(dyn, csv) / attach_rear_kinematics(...)
— false for Ld1/Ld2, true for Ld3.
158/158 tests pass (was 157).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Validates that the Ld4 framework supports multiple suspension topologies
through type-dispatched offline solvers — the C++ ISuspensionKinematics
runtime is type-agnostic (same CSV schema as DW).
tools/kinematics/mp_3d_solver.py:
- Strut top is a single ball joint → SK lies on intersection of two
spheres (around ST and around LK), giving a 1-D family. Knuckle has
one rotational DOF undetermined by pure geometry.
- scipy.optimize.least_squares with axis-angle rotation parameterization
+ light regularization (0.005 · |rotation|) eliminates spurious
far-flip basins.
- Continuation: each (travel, steer) sweep point warm-starts from the
nearest already-solved point. Produces smooth, monotone curves.
configs/suspensions/mp_front_sedan.yaml — sample sedan front geometry.
docs/tasks/T28_ld4_mp/ — REPORT + sweep CSV + 4-panel plot.
Limitation noted: this sample's hardpoints place the IC very close to the
wheel center → camber gain ~1°/mm (high). Real sedan tuning would place
IC ~0.6 m inboard for ~0.05°/mm gain — that's hardpoint design, not solver
bug.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The first MacPherson implementation modeled the strut as a fixed-length
rigid bar with ball joints at both ends:
|SK - ST| = L_strut (1 scalar constraint)
This treats strut bottom as a ball joint, which leaves the knuckle's
rotation about the strut axis under-determined. The least_squares had to
use a regularization hack + continuation to pick a unique solution, and
the resulting magnitudes were way off (camber gain ~1°/mm vs realistic
0.03°/mm).
The proper kinematic model: the strut is a CYLINDRICAL JOINT — tube
rigidly attached to knuckle, chassis-side shaft attached via spherical
top mount, telescoping + axial rotation allowed. The strut COMPRESSION
length is a free parameter (set by spring dynamics, not kinematics).
The kinematic constraint is that the tube axis line passes through the
chassis ST mount:
(SK_world - ST_chassis) × (R @ tube_axis_body) = 0
(3 scalars, 1 redundant since parallel-vector constraint has
only 2 independent dimensions)
This + tie rod (1 scalar) + LK fixed (3 from LCA + wheel travel input)
gives 6 constraints on knuckle's 6 DOFs — fully determined, no free DOF.
Result: single-shot LM converges to monotone smooth solutions without
continuation or regularization. Camber gain 0.034°/mm matches realistic
sedan MacPherson. Strut compression now correctly tracks 1:1 with
wheel travel.
Also added strut_compression to the output struct (diagnostic for spring
deflection — the variable the spring/damper acts on).
User-credit: explanation by user prompted this correction — pointing out
that real MacPherson constrains the knuckle's strut-axis rotation via
the rigid tube-to-knuckle attachment, which the original solver missed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…pprox) Both DW and MP solvers previously approximated wheel z as LK_z + offset_z when solving for the LCA angle. This ignores the knuckle's rotation R about kingpin — wheel center is rigidly offset from LK, so the true wheel z is LK_z + (R @ off_wheel)[z]. Measured approximation error: ~3 mm at ±0.1 rad LCA angle (≈ 50 mm wheel travel for typical DW geometry). Sub-mm at small travels. Fix in both solvers: factor out _kinematic_at(theta_l, ...) that performs the full kinematic solve (LCA + UCA/strut + tie rod) and returns wheel_pos. The _solve_lca_for_wheel_z Newton now iterates on the TRUE wheel_pos.z returned by _kinematic_at, not the approximated LK_z + offset. Verification after fix: DW: wheel-z error < 0.1 μm across ±80 mm travel range. MP: wheel-z error < 0.04 μm across ±80 mm travel range. Also refactored both solve() methods to consume the cached state from the final _kinematic_at call rather than re-running the geometry construction. User-credit: prompted by user's question "다른 구현도 이런 실수 있는것 아니야?" after fixing the MP cylindrical-joint constraint. 158/158 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stage E continued — Ld4 framework supports 4 suspension topologies through
type-dispatched offline solvers + universal CSV runtime:
tools/kinematics/ta_3d_solver.py — semi-trailing arm rear (1-DOF
revolute about chassis axis; semi-trailing tilt gives camber/toe gain).
tools/kinematics/fivelink_3d_solver.py — general 5-link rear. Knuckle
pose (6 DOF) solved via least_squares with 5 link-length residuals + 1
wheel-z residual; continuation across sweep. Most general multilink —
other types are special cases of this.
tools/kinematics/diagnose.py — auto-detects suspension type from YAML,
dispatches to the right solver, reports:
* solver success rate
* max wheel-z error (Newton residual)
* camber/toe/track monotonicity + average gain
* type-specific extras (IC for DW, strut tilt for MP, semi-trailing
angle for TA, link lengths for 5-link).
Exit 0 = consistent, 1 = problems.
tools/kinematics/import_hardpoints.py — neutral hardpoint exchange:
reads a CSV `name,x,y,z` with Adams-Car-style names (hpl_lca_outer
etc), auto-detects type, maps to VDSim YAML schema. Closes the gap
for users coming from Adams Car / commercial MBS tools.
configs/suspensions/ta_rear_sedan.yaml, 5link_rear_sports.yaml — samples.
docs/tasks/T29_ld4_ta, T30_ld4_5link — reports.
Verified all 4 types load + attach via ISuspensionKinematics:
TA travel=30mm: camber=-0.668° toe=+0.286°
5lnk travel=30mm: camber=+0.516° toe=-0.645°
vdsim.attach_rear_kinematics(dyn, ...) → True for Ld3.
158/158 tests still pass (no changes to C++ side — same runtime, new
offline data sources).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CARLA bridge (T22 update):
- vdsim_carla_bridge.py: BridgeConfig gains kinematics_{front,rear}_csv.
When level=L3, the bridge attaches the lookup tables to inner Ld3
via vdsim.attach_{front,rear}_kinematics().
- run_demo.py: new --kinematics_front / --kinematics_rear flags.
Sign-convention regression tests (T26):
+ FourteenDOF.WheelTravelSignConventionBumpIsPositive — verifies that
chassis-dive (front bump) gives positive wheel_travel input.
+ FourteenDOF.LeftRightCamberSymmetry — same lookup, both wheels in
straight-line bump, FL and FR get equal-and-opposite camber thrust Fy
(mirror symmetry).
Branch SUMMARY (docs/SUMMARY.md): pages T22-T30 tied together into one
landing-page view of all Ld4 work — diagram of offline-solver →
universal CSV → C++ ISuspensionKinematics flow, diagnostic results for
all 4 suspension types, tire-upgrade lineage, sign-convention regression
matrix.
End-to-end validation: CARLA Town10HD, sports.yaml with all
upgrades enabled (load_sens=0.15, relaxation_length_lat=0.6, DW front
kinematics, 5-link rear kinematics) — 600 ticks of figure-8 at 612 TPS.
160/160 tests pass (was 158).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
C++ native solver (core/src/dw_native_kinematics.cpp):
- In-process implementation of ISuspensionKinematics for double-wishbone:
* Newton on θ_lca for TRUE wheel z (matches Python solver's algorithm)
* UCA θ_u from Newton on |UK - LK| = L_LU constraint
* 3-sphere trilateration for TK (LK, UK, TR_inner spheres)
* Camber/toe/track/caster derived from knuckle frame
- Eigen3 + yaml-cpp. No offline CSV precompute needed.
- Mirrors tools/kinematics/dw_3d_solver.py byte-for-byte mathematically;
new test DWNativeKinematics.MatchesLookupAtGridPoints confirms agreement
to < 1e-3 rad at grid points.
- Python binding: vdsim.create_dw_native_kinematics(yaml_path).
- Tests use VDSIM_SOURCE_DIR compile-def for repo-relative paths
(relative paths broke under ctest's WORKING_DIRECTORY).
GUI editor (tools/kinematics/gui_editor.py):
- Interactive matplotlib widget tool: slider for each hardpoint coord,
live update of camber/toe/track curves on every drag, save back to YAML.
- Uses C++ native solver via Python binding (5-10x faster than Python
fallback) — falls back to dw_3d_solver.py if vdsim not built.
- Top + side views of hardpoints with annotated link chains.
162/162 tests pass (was 160).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…editor
C++ native solvers (3 new):
- core/src/ta_native_kinematics.cpp — Newton on single revolute angle.
- core/src/mp_native_kinematics.cpp — outer Newton on θ_lca + inner LM
on knuckle rotation R (cylindrical-joint constraint via cross product).
Includes a compact LM utility (numerical Jacobian, λ-modified normal
equations) shared with 5-link.
- core/src/fivelink_native_kinematics.cpp — LM on 6-DOF knuckle pose
(3 position + 3 axis-angle) with 5 link-length residuals + 1 wheel-z.
- All call vdsim::ISuspensionKinematics, so attach to L3 via the same
interface as the lookup table.
Python bindings:
- vdsim.create_{ta,mp,5link}_native_kinematics(yaml_path)
Tests (3 new):
- TANativeKinematics.MatchesLookupAtGridPoints
- MPNativeKinematics.MatchesLookupAtGridPoints
- FiveLinkNativeKinematics.MatchesLookupAtGridPoints
- All native solvers agree with Python lookup to < 5e-3 rad at grid
points (Newton convergence + LM convergence).
- **165/165 tests pass** (was 162).
Web-based hardpoint editor (viewer/):
- suspension_editor_server.py — websockets backend; auto-detects type,
dispatches to the right native solver, sends sweep + geometry to client.
- suspension_editor.html — Three.js 3D scene with hardpoint spheres + link
lines + wheel disc; right panel of curve plots (camber/toe/track);
left panel of editable hardpoint x/y/z fields with Δmm change tracking.
- Auto re-solves on every coordinate edit. Save button writes back to YAML.
- Tested with single update — server reports correct sweep after change.
Workflow now fully self-contained:
YAML hardpoints → native solver (C++) → web UI for design iteration
→ save back to YAML → L3 attach for simulation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This is the missing piece that lets VDSim claim its "OEM design validation" positioning: instead of single sims, run many parameter configurations through standard maneuvers and extract response surfaces. apps/doe/scenarios.py — three standard maneuvers: - step_steer (ISO 7401-ish; sudden steer at fixed speed) - steady_state_circular (hold steer until settled) - slow_ramp_steer (ISO 4138 understeer characterization) apps/doe/metrics.py — seven extractors: peak_yaw_rate, steady_state_yaw_rate, yaw_overshoot, yaw_settling_time, peak_lateral_g, steady_state_ay_per_steer, understeer_gradient_K. apps/doe/sweep_runner.py — entry point. Reads a sweep YAML, builds N combinations × M scenarios, overrides parameters via dotted paths (vehicle.*, tire.*, solver.*), runs each, extracts metrics, dumps: - CSV (one row per combo×scenario) - 1-D line plots when only one param varies - 2-D heatmaps for two-param sweeps - Sensitivity bar charts for 3+ params apps/doe/example_sweep.yaml — 4×4 grid (vehicle.cg_height × tire.D_lat), 2 step-steer scenarios, 5 metrics. 16 combos × 2 scenarios = 32 runs. Output: cg_height/D_lat × peak_lateral_g + yaw_overshoot + ... heatmaps. python/bindings.cpp — expose State.wheel_spin / susp_compression / susp_velocity (needed for proper init from Python). This closes one of the three "must-do" items called out in the positioning review: DOE workflow ✓ (this commit), validation suite (next), FMI export (next). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This is the 2nd of three positioning-completion items: standard maneuver
validation that lets us claim "산업 표준 검증" — measured metrics line
up with ISO definitions, runs against any VehicleParams + TireParams.
apps/validation/iso_7401.py — ISO 7401:2003 step-steer transient response:
- Step input with t_pre settle / t_post observation
- Extracts:
psi_dot_ss / psi_dot_peak [rad/s]
U = peak/SS overshoot ratio
T_max (time to peak)
T_psi_dot (time to 90% SS, ISO § 7.2)
Settling 5%
a_y_ss
apps/validation/iso_4138.py — ISO 4138:2012 steady-state circular driving:
- Slow ramp method (constant v, ramping steer)
- Computes δ_kin = L · r / v, then linear-fit (δ - δ_kin) vs a_y in the
1–4 m/s² linear region
- Outputs:
K = d(δ-δ_kin)/d(a_y) [rad·s²/m and mrad/g]
handling tendency (understeer / neutral / oversteer)
linear range extent
apps/validation/run_validation.py — orchestrator + report generator.
Runs both ISO tests on a (vehicle, tire, level) config; writes markdown
REPORT.md + per-test PNG.
Sample run (sports.yaml @ L3, 80 km/h):
ISO 7401: U=1.21 (20.6% overshoot), T_psi_dot=0.20s, settling=2.29s
ISO 4138: K=+9.69 mrad/g (UNDERSTEER, sports typical 5–15 mrad/g range)
These are now the canonical "is this vehicle config sensible?" tests —
also usable as DOE metric pipeline (compute K_understeer across 100
tire variations etc.).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Third of three positioning-completion items. VDSim's L2 dynamics
packaged as a standards-compliant .fmu loadable by CarMaker, dSPACE,
MATLAB/Simulink, FMPy, Modelon, OpenModelica, etc.
fmi_export/fmi2/ — minimal FMI 2.0 headers (types + functions) inlined
to avoid an external dependency (FMI is BSD-2 licensed by the Modelica
Association — attribution preserved).
fmi_export/vdsim_l2_fmu.cpp — C ABI implementation:
- fmi2Instantiate / FreeInstance — owns a VDSimL2 struct holding a
create_seven_dof() instance + cached I/O.
- fmi2EnterInitializationMode — loads vehicle.yaml + tire.yaml from
<fmuResourceLocation>/configs/ and initializes dynamics.
- fmi2DoStep — applies cached inputs to a CmdL4 and calls dyn->step()
with flat-ground contacts.
- fmi2SetReal / GetReal — maps ValueReferences (1..3 inputs, 10..17
outputs) to dynamics state.
- All other CS-required entry points provided (Reset, Terminate, ...).
fmi_export/modelDescription.xml — 3 inputs, 8 outputs, declared with
proper FMI 2.0 metadata (variability, causality, units, GUID match).
fmi_export/build_fmu.sh — bash script:
- Compiles fmu .so against the existing libvdsim_core.a + yaml-cpp + spdlog
- Uses -fvisibility=hidden + --version-script to expose ONLY fmi2*
symbols
- Copies resources (configs/vehicles/*.yaml + tires/*.yaml) into the FMU
- ZIPs into vdsim_l2.fmu (~4 MB output)
fmi_export/test_fmu_ctypes.py — smoke test via ctypes (no fmpy dependency
needed). Verified: Instantiate → InitializationMode (loads YAML) →
SetReal(throttle, steer) → 200 × DoStep(20ms) → GetReal returns
plausible state evolution.
Output: build/fmi_export/vdsim_l2.fmu (4.1 MB)
This makes VDSim usable as a vehicle-dynamics co-simulation slave in any
FMI 2.0 master (CarMaker, dSPACE SCALEXIO, Modelon Impact, OpenModelica,
MATLAB/Simulink, ANSYS Twin Builder, etc.) — the standard path into OEM
SIL/HIL workflows.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fmi_export/fmu_master.py — load any FMI 2.0 Co-Simulation .fmu via
ctypes only, no fmpy / fmi-library dependency:
fmu = FMUMaster.load("anything.fmu")
fmu.initialize(t0=0.0)
fmu.set("steer_angle_wheel", 0.05)
fmu.do_step(t=0.0, dt=0.02)
print(fmu.get("vx"))
Implementation:
- zipfile unzip → tempdir → reads modelDescription.xml for variable
map (name → ValueReference, type, causality, variability).
- ctypes.cdll.LoadLibrary on binaries/<platform>/<modelIdentifier>.so
- Wraps full FMI 2.0 CS lifecycle: Instantiate / SetupExperiment /
Enter/Exit InitializationMode / DoStep / Reset / FreeInstance.
- Variables accessed by NAME (vs by raw VR) — much friendlier.
Demonstrates the multi-backend strategy:
- Our vdsim_l2.fmu loaded back via this master (round-trip)
- Chrono Vehicle FMU export (when available) would load identically
- CarMaker / CarSim / Simulink FMU export → same pattern
- VDSim runtime can use external FMU as drop-in vehicle dynamics
fmi_export/test_roundtrip.py — regression:
Native VDSim L2 vs FMU-wrapped VDSim L2, same inputs/timestep:
max |Δ vx| = 0.000e+00
max |Δ vy| = 0.000e+00
max |Δ yaw_rate| = 0.000e+00
max |Δ ay| = 0.000e+00
PASS — outputs identical to numerical precision.
This closes the FMI loop:
- Export: build/fmi_export/vdsim_l2.fmu (industry tools can use VDSim)
- Import: FMUMaster.load(anything.fmu) (VDSim can use industry tools)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
L3 FMU (fmi_export/vdsim_l3_fmu.cpp + modelDescription_l3.xml + build_l3_fmu.sh):
- Extends L2 FMU to FourteenDOF dynamics
- Adds outputs: roll, pitch, susp_compression[4], Fz[4] (16 outputs total)
- Auto-attaches Ld4 hardpoint kinematics from resources/kinematics/{front,rear}.csv
- Build with FRONT_KIN_CSV / REAR_KIN_CSV env vars
- Result: build/fmi_export/vdsim_l3.fmu (4.6 MB) loads cleanly via fmu_master.py
ISO 3888-2 Double Lane Change (apps/validation/iso_3888.py):
- Cone course geometry per ISO spec (A 12m + B 13.5m + C 11m + D 12.5m + E 12m)
- Smooth raised-cosine transition waypoints (vs. linear ramp which caused PP
overshoot)
- Pure Pursuit path tracker + proportional vx control
- Metrics: speed loss, max lateral excursion vs target lane, peak yaw rate,
peak ay, pass/fail (speed loss < 2 km/h AND excursion < 1 m)
- Sample: sports.yaml @ 60 km/h FAILs (1.3m excursion, 5.5 km/h loss);
@ 40 km/h PASSes — matches real-world moose-test behavior.
- Integrated into run_validation.py — single command runs all 3 ISO tests
and generates markdown report with plots.
README.md rewrite (comprehensive):
- Positioning statement at top: 외부 시각화/센서 → CARLA 의존, 우리는
정확한 동역학 + 설계 검증 + FMI 통합
- Full Layout table covering all branches: core, kinematics tools, viewer,
CARLA, jump_demo, DOE, validation, FMI export, configs, tests
- 7-section Quick Tour: dynamics + control / hardpoint design / DOE / ISO
validation / FMI export / FMI import / CARLA bridge
- Architecture ASCII diagram showing external tools ↔ VDSim core ↔ Python/UI
- Validation status table (sports L3): U=1.21 / K=+9.69 mrad/g / DLC fail
@60 / pass @40 / FMU round-trip = 0
- 14-phase status (W1-W12 + Ld4 + DOE + ISO + FMI = phases 0-12 done)
- License clear: Apache-2.0 + FMI headers BSD-2
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Owner
Author
|
feat: PoC W1-W12 (95%) + Ld4 hardpoint framework + ISO validation + FMI 2.0 |
Onlyti
added a commit
that referenced
this pull request
Jun 9, 2026
First B2b increment (deep split touching encapsulated state). The mini-map owns its canvas + trail buffer (mm/mmCtx/mmTrail/MM_MAX, all const). Moved the whole cluster to static/minimap.js exporting drawMinimap() + resetMinimap(); the three external `mmTrail.length = 0` reset sites now call resetMinimap() so the trail state stays encapsulated. Only dependency is $ (from util.js). esbuild bundle gate (node20): output identical to pre-split except the resetMinimap indirection (semantically equal to mmTrail.length=0). Serves 200. Needs a browser glance: drive a vehicle, confirm the mini-map trail draws and clears on vehicle reset. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Onlyti
added a commit
that referenced
this pull request
Jun 9, 2026
….js step Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.
No description provided.