diff --git a/docs/roadmap/NAV-VEHICLE-PHYSICS-ROADMAP.md b/docs/roadmap/NAV-VEHICLE-PHYSICS-ROADMAP.md index 877ac989f..9a8260bee 100644 --- a/docs/roadmap/NAV-VEHICLE-PHYSICS-ROADMAP.md +++ b/docs/roadmap/NAV-VEHICLE-PHYSICS-ROADMAP.md @@ -96,6 +96,41 @@ The plan, in order: trained on the dynamic model beats the point-mass one over paired seeds. 4. **P8c:** a differentiable power model and energy in the loss, calibrated to the P3 ledger. +### P8a's vehicle fitted to Jolt (2026-10-20) + +`nav_plant::dyn_bike_si_of` now gives the dynamic model the values the Jolt plant's one-tick +residual asks for. Jolt runs of the Humvee and the RoboRacer were fitted on held-out seeds, which +made the twist residual about 11x lower. + +- **Tires.** Jolt's lateral friction ramps linearly to its peak at `tire_peak_slip_angle`: a + cornering stiffness of 1 / alpha_peak per unit load. A Pacejka tire peaking at the same angle + starts 4.5x steeper. B is now set so that B C = 1.12 / alpha_peak (`kJoltTireStiffness`). +- **Front and rear.** They are alike now (`front_stiffness` 1, was 0.85), since Jolt's tires have no + load sensitivity. +- **A direct drive** (one gear, no torque converter: the RoboRacer) gets no drive lag and no drag or + rolling resistance. The driver model's integral trim tracks the commanded acceleration, so the + command is the achieved acceleration. +- **A geared drive through a converter** (the Humvee) keeps its 0.1 s lag and its resistances. + +The P8b gate rerun on Jolt RoboRacers (city, 20 held-out seeds, paired bootstrap; Linux), before +and after: + +| training | dynamic-trained, before / after | dynamic - untrained seed, before | after | +|---|---|---|---| +| lr 5e-5, 150 steps | 50.8% / 51.6% | +0.2 [-1.8, +2.2] | +0.9 [-1.0, +3.0] | +| lr 1e-4, 300 steps | 48.5% / 52.1% | -2.1 [-4.2, +0.1] | +1.5 [-1.4, +4.1] | +| lr 2e-4, 300 steps | 43.1% / 53.8% | -7.5 [-10.0, -5.3] | +3.1 [+0.7, +5.5] | +| checkpoints selected on 32 validation seeds | 52.2% / 53.2% | +1.6 [-0.4, +3.6] | +2.6 [+0.7, +4.6] | + +- **The point-mass arm and the untrained seed are unchanged:** 51.4%, 49.9%, 22.2%, 52.2% for the + point mass, and 50.6% for the seed. +- **Training through the dynamic model now helps on Jolt** rather than only hurting less: + - its validation reach rises steadily over training (0.497 -> 0.526 at step 210), where before it + fell to 0.41; + - the trained policy beats the untrained one outside the CI at lr 2e-4 and with checkpoint + selection. +- **At the default learning rate the change is within the noise:** +0.7 [-1.4, +2.9]. + How the physics channels enter training: - **Per unit mass.** Each channel is defined per unit mass, for example grade as g·∇z converted with diff --git a/inc/cvc/physics/nav_plant.h b/inc/cvc/physics/nav_plant.h index cc6b84478..10265005e 100644 --- a/inc/cvc/physics/nav_plant.h +++ b/inc/cvc/physics/nav_plant.h @@ -121,15 +121,26 @@ class nav_plant final : public cvc::nav::vehicle_plant { // - mass; yaw inertia of the chassis box, m (hx^2 + hy^2) / 3; // - the centre of gravity's distances to the front (steered) and rear axles, and its height // as terrain_config_of has it; - // - Pacejka B and C from the tires' peak slip angle and sliding fraction: sin(C pi / 2) is - // tire_sliding, and the force peaks at tire_peak_slip_angle. vehicle_desc has one tire, - // so `front_stiffness` scales the front's B: a road vehicle's understeer comes from the - // front's load sensitivity and steering compliance, which the single track leaves out; + // - Pacejka C from the tires' sliding fraction (sin(C pi / 2) is tire_sliding) and B fitted to + // how Jolt's tires corner. Jolt's lateral friction ramps linearly to its peak at + // tire_peak_slip_angle, a cornering stiffness of 1 / alpha_peak per unit load, and a Pacejka + // tire peaking at the same angle starts 4.5x steeper (B C = C tan(pi / 2C) / alpha_peak). + // So B C = k / alpha_peak with k = kJoltTireStiffness (1.12), fitted to the one-tick + // residual of Jolt runs of the Humvee and the RoboRacer (libcvc #749's fit_dyn_bike; + // held-out twist residual about 11x lower than with the 4.5x tire). `front_stiffness` + // scales the front's B; Jolt's tires have no load sensitivity or steering compliance, and + // the fit puts front and rear alike (1); // - the drive split from which axles are driven, the brake split from the axles' brake // torques; drag 1/2 rho C_d A and the rolling resistance; - // - the steering's rate; `drive_lag` for the force (nav_plant_options::latency's default). + // - the steering's rate; `drive_lag` for the force, < 0 for the vehicle's own: a direct drive + // (one gear, no torque converter) answers within the tick (0), a geared drive through a + // converter takes 0.1 s (nav_plant_options::latency's default); + // - for a direct drive, no drag or rolling resistance: the driver model's integral trim tracks + // the commanded acceleration and makes up for them, so the commanded acceleration is the + // achieved one (the RoboRacer's fit: lag 0, resistance x0; the Humvee's keeps both). + static constexpr double kJoltTireStiffness = 1.12; static cvc::nav::dyn_bike_si dyn_bike_si_of(const vehicle_desc &d, double mu = 0.9, - double front_stiffness = 0.85, double drive_lag = 0.1, + double front_stiffness = 1.0, double drive_lag = -1.0, double gravity = 9.80665); // The same vehicle in a normalized frame where its wheelbase is `wheelbase`: S = diff --git a/src/cvc/physics/nav_plant.cpp b/src/cvc/physics/nav_plant.cpp index 3495e9207..d8be0a7f8 100644 --- a/src/cvc/physics/nav_plant.cpp +++ b/src/cvc/physics/nav_plant.cpp @@ -314,19 +314,24 @@ cvc::nav::dyn_bike_si nav_plant::dyn_bike_si_of(const vehicle_desc &d, double mu si.cg_to_rear = std::max(1e-3, d.center_of_mass.x - rear); si.cg_height = std::max(0.0, -z + travel + radius + d.center_of_mass.z); si.mu = mu; - // Pacejka: sin(C pi / 2) = tire_sliding, C in (1, 2); the peak at B alpha = tan(pi / (2 C)). + // Pacejka: sin(C pi / 2) = tire_sliding, C in (1, 2). B makes the cornering stiffness per unit + // load, B C, kJoltTireStiffness times Jolt's: its lateral friction ramps linearly to 1 at + // tire_peak_slip_angle (jolt_backend.cpp). const double kPi = 3.14159265358979323846; const double sliding = std::min(std::max(d.tire_sliding, 0.05), 0.999); si.tire_C = 2.0 - 2.0 * std::asin(sliding) / kPi; - const double B = std::tan(kPi / (2.0 * si.tire_C)) / std::max(d.tire_peak_slip_angle, 1e-3); + const double B = kJoltTireStiffness / (si.tire_C * std::max(d.tire_peak_slip_angle, 1e-3)); si.tire_B_front = B * front_stiffness; si.tire_B_rear = B; si.front_drive = front_driven && rear_driven ? 0.5 : (front_driven ? 1.0 : 0.0); si.front_brake = brake_front + brake_rear > 0.0 ? brake_front / (brake_front + brake_rear) : 0.5; - si.drag = 0.5 * d.air_density * d.drag_coefficient * d.frontal_area; - si.rolling_resistance = d.rolling_resistance; + // A direct drive: one gear, no torque converter. The driver model's trim makes its commanded + // acceleration the achieved one within the tick, drag and rolling resistance included. + const bool direct = d.gear_ratios.size() <= 1 && !(d.converter_k > 0.0); + si.drag = direct ? 0.0 : 0.5 * d.air_density * d.drag_coefficient * d.frontal_area; + si.rolling_resistance = direct ? 0.0 : d.rolling_resistance; si.steer_rate = d.max_steer_rate; - si.drive_lag = drive_lag; + si.drive_lag = drive_lag >= 0.0 ? drive_lag : (direct ? 0.0 : 0.1); si.gravity = gravity; return si; } diff --git a/src/cvc/tests/data/dyn_bike_golden.json b/src/cvc/tests/data/dyn_bike_golden.json index 6a8f920ff..5801024a0 100644 --- a/src/cvc/tests/data/dyn_bike_golden.json +++ b/src/cvc/tests/data/dyn_bike_golden.json @@ -1,43 +1,43 @@ { "format": "cvc-dyn-bike-golden-1", "order": ["x", "y", "psi", "vx", "vy", "r", "delta", "a"], - "params": {"m": 2676, "Iz": 6283.69384765625, "lf": 1.5369999408721924, "lr": 1.7630000114440918, "h": 0.74000000953674316, "g": 9.8066501617431641, "mu": 0.89999997615814209, "B_f": 36.121696472167969, "B_r": 42.496116638183594, "C": 1.2021652460098267, "front_drive": 0.5, "front_brake": 0.70588237047195435, "drag": 1.5006250143051147, "c_rr": 0.014999999664723873, "tau_steer": 0.05000000074505806, "steer_rate": 0.34999999403953552, "tau_drive": 0.10000000149011612, "v_blend": 1, "v_width": 0.30000001192092896, "eps": 0.5, "fz_min": 0.05000000074505806, "iters": 3}, + "params": {"m": 2676, "Iz": 6283.69384765625, "lf": 1.5369999408721924, "lr": 1.7630000114440918, "h": 0.74000000953674316, "g": 9.8066501617431641, "mu": 0.89999997615814209, "B_f": 10.708646774291992, "B_r": 10.708646774291992, "C": 1.2021652460098267, "front_drive": 0.5, "front_brake": 0.70588237047195435, "drag": 1.5006250143051147, "c_rr": 0.014999999664723873, "tau_steer": 0.05000000074505806, "steer_rate": 0.34999999403953552, "tau_drive": 0.10000000149011612, "v_blend": 1, "v_width": 0.30000001192092896, "eps": 0.5, "fz_min": 0.05000000074505806, "iters": 3}, "g_out": [0.69999999999999996, -0.29999999999999999, 1.1000000000000001, 0.40000000000000002, -0.90000000000000002, 0.25, 0.59999999999999998, -0.14999999999999999], "cases": [ {"state": [0, 0, 0, 12, 0.10000000000000001, 0.050000000000000003, 0.02, 0.5], "delta_cmd": 0.050000000000000003, "a_cmd": 1, "grade": 0, "limits": {"L": 3.2999999523162842, "vmax": 40, "hdt": 0.059999998658895493, "allow_reverse": 0}, - "next": [0.72161232476023529, 0.0030915364619978677, 0.006158445274966143, 12.026961596240618, 0.15841392030776888, 0.10264075687696875, 0.035978411112368727, 0.72559417581953878], - "g_state": [0.69999999999999996, -0.29999999999999999, 0.881352227048531, 0.43908416096250991, -0.30332956174135622, 0.11778155321430109, -1.2584661505628023, -0.067429376024266241], - "g_delta_cmd": -0.52467945254116677, "g_a_cmd": -0.05543495498191852}, + "next": [0.72170075978362558, 0.0014655898551522932, 0.0048612338851567365, 12.028322883477824, 0.10879309514548365, 0.08102056656356306, 0.035978411112368727, 0.72559417581953878], + "g_state": [0.69999999999999996, -0.29999999999999999, 0.88246385916630588, 0.44344941191751436, -0.58216522580427366, 0.44787693050917154, -0.31518187358286542, -0.067550279387271248], + "g_delta_cmd": -0.13140556288176811, "g_a_cmd": -0.055534351904751283}, {"state": [1, -2, 0.29999999999999999, 15, -1.2, 0.59999999999999998, 0.25, 0], "delta_cmd": 0.29999999999999999, "a_cmd": 0, "grade": 0.02, "limits": {"L": 3.2999999523162842, "vmax": 40, "hdt": 0.059999998658895493, "allow_reverse": 0}, - "next": [1.8862789801906317, -1.8347158945971369, 0.33547355793444861, 14.854786313864151, -1.2194566888495753, 0.59122597878907412, 0.26954533644854789, 0], - "g_state": [0.69999999999999996, -0.29999999999999999, 0.71841743216080634, 0.46493637589482706, -0.92038287845088973, 1.1426390353048035, 0.4563570614107727, -0.077760090566516074], - "g_delta_cmd": 0.04704770737140862, "g_a_cmd": -0.063928029207825282}, + "next": [1.8867829073011033, -1.8363951317071947, 0.3362258629537182, 14.851775313286971, -1.2377173130869106, 0.60376439605715648, 0.26954533644854789, 0], + "g_state": [0.69999999999999996, -0.29999999999999999, 0.71944172000470541, 0.4699842462121625, -0.88320019268164862, 1.0507783479334352, 0.44490310874960692, -0.076361021377209612], + "g_delta_cmd": 0.045866872760495392, "g_a_cmd": -0.062777828181229955}, {"state": [-3, 4, -1.1000000000000001, 14, 0.80000000000000004, 0.40000000000000002, 0.14999999999999999, -4], "delta_cmd": 0.14999999999999999, "a_cmd": -6, "grade": -0.029999999999999999, "limits": {"L": 3.2999999523162842, "vmax": 40, "hdt": 0.059999998658895493, "allow_reverse": 0}, - "next": [-2.6139870110986414, 3.2746823456478453, -1.070979653140653, 13.693204346336563, 0.7055992128641444, 0.48367245846670809, 0.14999999999999999, -4.9023767032781551], - "g_state": [0.69999999999999996, -0.29999999999999999, 1.4919184613761005, 0.44997106717466218, -0.51421353207102205, 0.22798404545675632, 0.037739398844601396, -0.076678056992082619], - "g_delta_cmd": 0.087559812311952701, "g_a_cmd": -0.063038469107698336}, + "next": [-2.6188025127373895, 3.2714360882174414, -1.0723766257472249, 13.702551861063213, 0.58732269798076042, 0.4603895811700956, 0.14999999999999999, -4.9023767032781551], + "g_state": [0.69999999999999996, -0.29999999999999999, 1.495635492069008, 0.45176658307929768, -0.64916167547811943, 0.61889519194170228, -0.0099817306380819981, -0.073112649574912106], + "g_delta_cmd": -0.023158780690115677, "g_a_cmd": -0.060107280784200925}, {"state": [2, 2, 2.5, 10, 2.5, 1.1000000000000001, 0.40000000000000002, 1], "delta_cmd": 0.45000000000000001, "a_cmd": 2, "grade": 0, "limits": {"L": 3.2999999523162842, "vmax": 40, "hdt": 0.059999998658895493, "allow_reverse": 0}, - "next": [1.4901741447892105, 2.3297160167472559, 2.5727797258014662, 10.119081136118659, 2.0853344678711072, 1.2129954571369974, 0.41954533644854791, 1.4511883516390776], - "g_state": [0.69999999999999996, -0.29999999999999999, 1.0221465448401579, 0.3680655773155822, -0.4105335944971768, -0.071966286436913274, 0.0064365975211407855, -0.068908097463021359], - "g_delta_cmd": 0.00066357504298502484, "g_a_cmd": -0.056650639617034933}, + "next": [1.4909879252982987, 2.3339382319443089, 2.5710190927434646, 10.145752585238649, 1.9848686819797339, 1.1836515721810814, 0.41954533644854791, 1.4511883516390776], + "g_state": [0.69999999999999996, -0.29999999999999999, 1.0189468600494942, 0.36877586997421102, -0.5469687755509326, 0.34522820509560315, -0.2583148462334196, -0.07068503726719233], + "g_delta_cmd": -0.026630729143777142, "g_a_cmd": -0.058111495164835755}, {"state": [0.5, 0.5, 0.20000000000000001, -2, 0.050000000000000003, -0.10000000000000001, 0.20000000000000001, -0.5], "delta_cmd": 0.25, "a_cmd": -0.80000000000000004, "grade": 0, "limits": {"L": 3.2999999523162842, "vmax": 40, "hdt": 0.059999998658895493, "allow_reverse": 1}, - "next": [0.38234157507615946, 0.47820894213237564, 0.19215389327768181, -1.9942410241949737, -0.21255053807775098, -0.13076844829487239, 0.2195453364485479, -0.63535650549172329], - "g_state": [0.69999999999999996, -0.29999999999999999, 1.1505512679844894, 0.37186240076506216, -0.12784613682710022, 0.017428116952459605, 1.2345138787580188, -0.07080295209586783], - "g_delta_cmd": 0.12727106168621422, "g_a_cmd": -0.058208435157532337}, + "next": [0.38163390582407375, 0.4801677888170704, 0.19227964555052293, -1.9995770449242993, -0.17430505462442358, -0.12867257703400767, 0.2195453364485479, -0.63535650549172329], + "g_state": [0.69999999999999996, -0.29999999999999999, 1.1493923760808287, 0.39264945863371031, -0.21968909271960391, 0.10685582785702351, 1.1849225895596516, -0.070144184766086387], + "g_delta_cmd": 0.12215849378781682, "g_a_cmd": -0.057666850177465923}, {"state": [0, 0, 0, 0.59999999999999998, 0.02, 0.050000000000000003, 0.29999999999999999, 0.29999999999999999], "delta_cmd": 0.34999999999999998, "a_cmd": 0.5, "grade": 0, "limits": {"L": 3.2999999523162842, "vmax": 40, "hdt": 0.059999998658895493, "allow_reverse": 0}, - "next": [0.035991441415742745, 8.9957242432581056e-05, 0.003613523021362957, 0.5998588717055453, 0.10550903842574866, 0.060225385035524874, 0.31954533644854788, 0.39023767032781553], - "g_state": [0.69999999999999996, -0.29999999999999999, 1.0891395975055744, 0.29781652332996111, 0.050398345344285118, 0.081666464100695529, 0.26493307713883441, -0.072170287614980372], - "g_delta_cmd": 0.027313029511808661, "g_a_cmd": -0.059332547338548272}, + "next": [0.036071137283442252, -1.6430282470171074e-05, 0.0036051455458450143, 0.60118074078138972, 0.10349000305763768, 0.060085760440438306, 0.31954533644854788, 0.39023767032781553], + "g_state": [0.69999999999999996, -0.29999999999999999, 1.0891901600126965, 0.30814654633137256, 0.022983162019829553, 0.083479081063051172, 0.27490937611823435, -0.071961486030844291], + "g_delta_cmd": 0.028341526788878316, "g_a_cmd": -0.059160887639182654}, {"state": [0, 0, 0, 0, 0, 0, 0, 0], "delta_cmd": 0.10000000000000001, "a_cmd": 2, "grade": 0, "limits": {"L": 3.2999999523162842, "vmax": 40, "hdt": 0.059999998658895493, "allow_reverse": 0}, - "next": [0.0032481983257981982, 5.3123536200254369e-08, 2.062374045329597e-05, 0.054136639980098399, 0.0006057631500759013, 0.00034372901523787501, 0.020946006673345115, 0.90237670327815533], - "g_state": [0.69999999999999996, -0.29999999999999999, 1.0990255033157852, 0.42620268045264031, 0.0018837905264509067, 0.0068176134156984324, 0.57682703919426437, -0.068034399515581628], - "g_delta_cmd": 0.0020775379242710104, "g_a_cmd": -0.055932356144165696} + "next": [0.0032481985805457771, 1.4550464025321322e-08, 2.063503148721843e-05, 0.054136644212622924, 0.00060545142240215992, 0.00034391719914078894, 0.020946006673345115, 0.90237670327815533], + "g_state": [0.69999999999999996, -0.29999999999999999, 1.0990255302405116, 0.42627209229010737, -0.0055533005664103159, 0.0088728813432996916, 0.57684392487435543, -0.068034179542302314], + "g_delta_cmd": 0.0020775987408388598, "g_a_cmd": -0.055932175300006146} ] } diff --git a/src/cvc/tests/nav_dyn_bike_test.cpp b/src/cvc/tests/nav_dyn_bike_test.cpp index 56d007373..d2a0ae7aa 100644 --- a/src/cvc/tests/nav_dyn_bike_test.cpp +++ b/src/cvc/tests/nav_dyn_bike_test.cpp @@ -74,14 +74,18 @@ template dyn_state at_rest(S vx = S(0)) { // Steady-state cornering at speed v and steer delta: hold the speed with a proportional // throttle for `seconds`, then average the last second. Returns the lateral acceleration -// vx r and the path's curvature r / vx. +// vx r and the path's curvature r / vx. `a_body`, when given, receives the body-frame lateral +// acceleration over that second, vy' + r vx: vx r alone is it only in a steady turn, and past +// its limit a vehicle drifts. void steady_turn(const dyn_bike_params &p, double v, double delta, double &a_y, double &kappa, - double seconds = 12.0) { + double seconds = 12.0, double *a_body = nullptr) { const bike_veh lim = plant_limits(p); dyn_state s = at_rest(v); const int n = static_cast(seconds / lim.hdt), tail = static_cast(1.0 / lim.hdt); - double ay = 0.0, k = 0.0; + double ay = 0.0, k = 0.0, vy0 = 0.0; for (int t = 0; t < n; ++t) { + if (t == n - tail) + vy0 = s.vy; dyn_state o; const double a_cmd = 2.0 * (v - s.vx) + p.drag * s.vx * s.vx / p.m + p.c_rr * p.g; diff::dyn_plant(p, lim, s, delta, a_cmd, 0.0, o); @@ -93,6 +97,8 @@ void steady_turn(const dyn_bike_params &p, double v, double delta, double &a_y, } a_y = ay / tail; kappa = k / tail; + if (a_body) + *a_body = a_y + (s.vy - vy0) / (tail * lim.hdt); } // The trainer's city scene, its field and the trainer's bicycle (coef_train's defaults). @@ -151,12 +157,26 @@ TEST(DynBike, HumveeParametersFromItsSpecSheet) { EXPECT_GT(si.tire_C, 1.0); EXPECT_LT(si.tire_C, 2.0); EXPECT_NEAR(std::sin(si.tire_C * 3.14159265358979 / 2.0), d.tire_sliding, 1e-9); - // The peak of mu Fz sin(C atan(B alpha)) is at the tire's peak slip angle (rear tires). - EXPECT_NEAR(si.tire_C * std::atan(si.tire_B_rear * d.tire_peak_slip_angle), - 3.14159265358979 / 2.0, 1e-9); - EXPECT_LT(si.tire_B_front, si.tire_B_rear); + // The cornering stiffness per unit load, B C, is kJoltTireStiffness times Jolt's linear ramp + // to its peak, 1 / alpha_peak; front and rear alike. + EXPECT_NEAR(si.tire_B_rear * si.tire_C * d.tire_peak_slip_angle, + cvc::physics::nav_plant::kJoltTireStiffness, 1e-9); + EXPECT_DOUBLE_EQ(si.tire_B_front, si.tire_B_rear); EXPECT_DOUBLE_EQ(si.front_drive, 0.5); // the Humvee drives all four wheels + // A geared drive through a torque converter: the force lags, drag and rolling resistance stay. + EXPECT_DOUBLE_EQ(si.drive_lag, 0.1); EXPECT_GT(si.drag, 0.0); + EXPECT_GT(si.rolling_resistance, 0.0); + // The RoboRacer's direct drive answers within the tick, the driver model making up for drag and + // rolling resistance; an explicit drive lag still wins. + const cvc::physics::vehicle_desc rr = cvc::physics::vehicle_desc::roboracer(); + const cvc::nav::dyn_bike_si sr = cvc::physics::nav_plant::dyn_bike_si_of(rr); + EXPECT_DOUBLE_EQ(sr.drive_lag, 0.0); + EXPECT_DOUBLE_EQ(sr.drag, 0.0); + EXPECT_DOUBLE_EQ(sr.rolling_resistance, 0.0); + EXPECT_DOUBLE_EQ(cvc::physics::nav_plant::dyn_bike_si_of(rr, 0.9, 1.0, 0.05).drive_lag, 0.05); + EXPECT_NEAR(sr.tire_B_rear * sr.tire_C * rr.tire_peak_slip_angle, + cvc::physics::nav_plant::kJoltTireStiffness, 1e-9); std::printf(" Humvee: %.0f kg, Iz %.0f kg m^2, lf %.2f m, lr %.2f m, h %.2f m, B %.1f/%.1f, " "C %.3f, brake front %.2f\n", si.mass, si.yaw_inertia, si.cg_to_front, si.cg_to_rear, si.cg_height, si.tire_B_front, @@ -499,14 +519,16 @@ TEST(DynBike, OversteerWhenBrakingInATurn) { } // Gate 4c: however hard it is steered, the steady lateral acceleration stays under mu g. +// The lateral acceleration is the body's, vy' + r vx: past its limit the vehicle drifts, where vx r +// alone overstates it. TEST(DynBike, SteadyLateralNeverExceedsMuG) { for (const double mu : {0.4, 0.9}) { const dyn_bike_params p = humvee(mu); double peak = 0.0; for (double delta = 0.05; delta <= 0.6; delta += 0.05) { - double ay, kappa; - steady_turn(p, 20.0, delta, ay, kappa, 15.0); - peak = std::max(peak, std::fabs(ay)); + double ay, kappa, a_body; + steady_turn(p, 20.0, delta, ay, kappa, 15.0, &a_body); + peak = std::max(peak, std::fabs(a_body)); } std::printf(" mu %.1f: the steadiest lateral acceleration %.2f m/s^2 against mu g %.2f\n", mu, peak, mu * kG);