1#![allow(clippy::needless_range_loop)]
2#![allow(missing_docs)]
12#![allow(dead_code)]
13
14use serde::{Deserialize, Serialize};
15
16pub fn pacejka_fy(alpha: f64, b: f64, c: f64, d: f64, e: f64) -> f64 {
31 let x = alpha.to_degrees(); let phi = (1.0 - e) * x + (e / b) * (b * x).atan();
33 d * (c * (b * phi).atan()).sin()
34}
35
36pub fn pacejka_fx(kappa: f64, b: f64, c: f64, d: f64, e: f64) -> f64 {
44 let x = kappa * 100.0; let phi = (1.0 - e) * x + (e / b) * (b * x).atan();
46 d * (c * (b * phi).atan()).sin()
47}
48
49pub fn compute_slip_angle(vy: f64, vx: f64) -> f64 {
56 if vx.abs() < 1e-4 {
57 return 0.0;
58 }
59 (-vy / vx).atan()
60}
61
62pub fn compute_slip_ratio(wheel_speed: f64, vehicle_speed: f64) -> f64 {
69 let v_ref = vehicle_speed.abs().max(wheel_speed.abs()).max(1e-4);
70 (wheel_speed - vehicle_speed) / v_ref
71}
72
73pub fn ackermann_angles(steer_angle: f64, wheelbase: f64, track_width: f64) -> (f64, f64) {
81 if steer_angle.abs() < 1e-9 {
82 return (0.0, 0.0);
83 }
84 let r = wheelbase / steer_angle.abs();
85 let sign = steer_angle.signum();
86 let inner = (wheelbase / (r - track_width * 0.5)).atan() * sign;
87 let outer = (wheelbase / (r + track_width * 0.5)).atan() * sign;
88 if steer_angle > 0.0 {
89 (inner, outer)
90 } else {
91 (outer, inner)
92 }
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct PacejkaCoeffs {
102 pub b: f64,
104 pub c: f64,
106 pub d: f64,
108 pub e: f64,
110}
111
112impl PacejkaCoeffs {
113 pub fn road_lateral() -> Self {
115 Self {
116 b: 10.0,
117 c: 1.9,
118 d: 1.0,
119 e: 0.97,
120 }
121 }
122
123 pub fn road_longitudinal() -> Self {
125 Self {
126 b: 11.0,
127 c: 1.65,
128 d: 1.0,
129 e: 0.0,
130 }
131 }
132
133 pub fn slick_lateral() -> Self {
135 Self {
136 b: 12.0,
137 c: 2.0,
138 d: 1.4,
139 e: 0.95,
140 }
141 }
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct FialaParams {
147 pub cornering_stiffness: f64,
149 pub mu: f64,
151}
152
153impl FialaParams {
154 pub fn new(cornering_stiffness: f64, mu: f64) -> Self {
156 Self {
157 cornering_stiffness,
158 mu,
159 }
160 }
161
162 pub fn lateral_force(&self, alpha: f64, fz: f64) -> f64 {
164 let f_max = self.mu * fz;
165 let f_lin = -self.cornering_stiffness * alpha;
166 if f_lin.abs() > f_max {
167 f_max * f_lin.signum()
168 } else {
169 f_lin
170 }
171 }
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
176pub enum TireModelKind {
177 Pacejka {
179 lateral: PacejkaCoeffs,
180 longitudinal: PacejkaCoeffs,
181 },
182 Fiala(FialaParams),
184 Linear {
186 cornering_stiffness: f64,
187 longitudinal_stiffness: f64,
188 },
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize)]
193pub struct PyTireModel {
194 pub kind: TireModelKind,
196 pub radius: f64,
198 pub width: f64,
200 pub unloaded_radius: f64,
202}
203
204impl PyTireModel {
205 pub fn road_pacejka(radius: f64) -> Self {
207 Self {
208 kind: TireModelKind::Pacejka {
209 lateral: PacejkaCoeffs::road_lateral(),
210 longitudinal: PacejkaCoeffs::road_longitudinal(),
211 },
212 radius,
213 width: 0.225,
214 unloaded_radius: radius,
215 }
216 }
217
218 pub fn slick_pacejka(radius: f64) -> Self {
220 Self {
221 kind: TireModelKind::Pacejka {
222 lateral: PacejkaCoeffs::slick_lateral(),
223 longitudinal: PacejkaCoeffs::road_longitudinal(),
224 },
225 radius,
226 width: 0.300,
227 unloaded_radius: radius,
228 }
229 }
230
231 pub fn compute_forces(&self, alpha: f64, kappa: f64, fz: f64) -> (f64, f64) {
235 match &self.kind {
236 TireModelKind::Pacejka {
237 lateral,
238 longitudinal,
239 } => {
240 let fy = pacejka_fy(alpha, lateral.b, lateral.c, lateral.d, lateral.e) * fz;
241 let fx = pacejka_fx(
242 kappa,
243 longitudinal.b,
244 longitudinal.c,
245 longitudinal.d,
246 longitudinal.e,
247 ) * fz;
248 (fx, fy)
249 }
250 TireModelKind::Fiala(params) => {
251 let fy = params.lateral_force(alpha, fz);
252 let fx = 0.0;
253 (fx, fy)
254 }
255 TireModelKind::Linear {
256 cornering_stiffness,
257 longitudinal_stiffness,
258 } => {
259 let fy = -cornering_stiffness * alpha;
260 let fx = longitudinal_stiffness * kappa * fz;
261 (fx, fy)
262 }
263 }
264 }
265}
266
267#[derive(Debug, Clone, Serialize, Deserialize)]
273pub struct PyTireThermal {
274 pub surface_temp: f64,
276 pub bulk_temp: f64,
278 pub carcass_temp: f64,
280 pub ambient_temp: f64,
282 pub k_surface_bulk: f64,
284 pub k_bulk_carcass: f64,
286 pub k_carcass_ambient: f64,
288 pub optimal_temp: f64,
290 pub optimal_window: f64,
292}
293
294impl PyTireThermal {
295 pub fn new() -> Self {
297 Self {
298 surface_temp: 25.0,
299 bulk_temp: 25.0,
300 carcass_temp: 25.0,
301 ambient_temp: 25.0,
302 k_surface_bulk: 50.0,
303 k_bulk_carcass: 20.0,
304 k_carcass_ambient: 5.0,
305 optimal_temp: 90.0,
306 optimal_window: 20.0,
307 }
308 }
309
310 pub fn update(&mut self, power: f64, dt: f64) {
312 let mass_surface = 0.3; let mass_bulk = 2.0;
314 let mass_carcass = 5.0;
315 let cp = 1300.0; let q_surface_bulk = self.k_surface_bulk * (self.surface_temp - self.bulk_temp);
318 let q_bulk_carcass = self.k_bulk_carcass * (self.bulk_temp - self.carcass_temp);
319 let q_carcass_ambient = self.k_carcass_ambient * (self.carcass_temp - self.ambient_temp);
320
321 self.surface_temp += (power - q_surface_bulk) / (mass_surface * cp) * dt;
322 self.bulk_temp += (q_surface_bulk - q_bulk_carcass) / (mass_bulk * cp) * dt;
323 self.carcass_temp += (q_bulk_carcass - q_carcass_ambient) / (mass_carcass * cp) * dt;
324 }
325
326 pub fn grip_scale(&self) -> f64 {
328 let dt = (self.surface_temp - self.optimal_temp).abs();
329 if dt <= self.optimal_window * 0.5 {
330 1.0
331 } else {
332 let excess = dt - self.optimal_window * 0.5;
333 (1.0 - excess / self.optimal_window).max(0.3)
334 }
335 }
336}
337
338impl Default for PyTireThermal {
339 fn default() -> Self {
340 Self::new()
341 }
342}
343
344#[derive(Debug, Clone, Serialize, Deserialize)]
350pub struct EngineCurve {
351 pub rpm: Vec<f64>,
353 pub torque: Vec<f64>,
355 pub redline: f64,
357 pub idle: f64,
359}
360
361impl EngineCurve {
362 pub fn flat(peak_torque: f64, redline: f64) -> Self {
364 Self {
365 rpm: vec![0.0, redline * 0.3, redline * 0.7, redline],
366 torque: vec![
367 peak_torque * 0.6,
368 peak_torque,
369 peak_torque,
370 peak_torque * 0.8,
371 ],
372 redline,
373 idle: 800.0,
374 }
375 }
376
377 pub fn torque_at(&self, rpm: f64) -> f64 {
379 let n = self.rpm.len();
380 if n == 0 {
381 return 0.0;
382 }
383 if rpm <= self.rpm[0] {
384 return self.torque[0];
385 }
386 if rpm >= self.rpm[n - 1] {
387 return self.torque[n - 1];
388 }
389 for i in 1..n {
390 if rpm <= self.rpm[i] {
391 let t = (rpm - self.rpm[i - 1]) / (self.rpm[i] - self.rpm[i - 1]);
392 return self.torque[i - 1] + t * (self.torque[i] - self.torque[i - 1]);
393 }
394 }
395 self.torque[n - 1]
396 }
397}
398
399#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
401pub enum DiffType {
402 Open,
404 Lsd,
406 Locked,
408}
409
410#[derive(Debug, Clone, Serialize, Deserialize)]
412pub struct PyDrivetrain {
413 pub engine_curve: EngineCurve,
415 pub gear_ratios: Vec<f64>,
417 pub final_drive: f64,
419 pub current_gear: usize,
421 pub diff_type: DiffType,
423 pub clutch: f64,
425 pub engine_rpm: f64,
427 pub engine_inertia: f64,
429}
430
431impl PyDrivetrain {
432 pub fn six_speed(peak_torque: f64) -> Self {
434 Self {
435 engine_curve: EngineCurve::flat(peak_torque, 7000.0),
436 gear_ratios: vec![3.31, 2.13, 1.55, 1.21, 0.97, 0.79],
437 final_drive: 3.73,
438 current_gear: 1,
439 diff_type: DiffType::Open,
440 clutch: 1.0,
441 engine_rpm: 1000.0,
442 engine_inertia: 0.2,
443 }
444 }
445
446 pub fn wheel_torque(&self, throttle: f64) -> f64 {
448 let engine_torque = self.engine_curve.torque_at(self.engine_rpm) * throttle;
449 let gear_ratio = self
450 .gear_ratios
451 .get(self.current_gear)
452 .copied()
453 .unwrap_or(1.0);
454 engine_torque * gear_ratio * self.final_drive * self.clutch
455 }
456
457 pub fn upshift(&mut self) {
459 if self.current_gear + 1 < self.gear_ratios.len() {
460 self.current_gear += 1;
461 }
462 }
463
464 pub fn downshift(&mut self) {
466 if self.current_gear > 0 {
467 self.current_gear -= 1;
468 }
469 }
470
471 pub fn current_ratio(&self) -> f64 {
473 self.gear_ratios
474 .get(self.current_gear)
475 .copied()
476 .unwrap_or(1.0)
477 }
478
479 pub fn update_rpm(&mut self, wheel_angular_velocity: f64) {
481 let ratio = self.current_ratio() * self.final_drive;
482 self.engine_rpm = (wheel_angular_velocity * ratio * 60.0 / (2.0 * std::f64::consts::PI))
483 .max(self.engine_curve.idle)
484 .min(self.engine_curve.redline);
485 }
486}
487
488#[derive(Debug, Clone, Serialize, Deserialize)]
494pub struct PySteering {
495 pub rack_ratio: f64,
497 pub max_wheel_angle: f64,
499 pub four_ws: bool,
501 pub rear_steer_ratio: f64,
503 pub wheelbase: f64,
505 pub track_width: f64,
507 pub steering_damping: f64,
509 pub input: f64,
511}
512
513impl PySteering {
514 pub fn new(wheelbase: f64, track_width: f64) -> Self {
516 Self {
517 rack_ratio: 0.15,
518 max_wheel_angle: 0.5,
519 four_ws: false,
520 rear_steer_ratio: 0.0,
521 wheelbase,
522 track_width,
523 steering_damping: 5.0,
524 input: 0.0,
525 }
526 }
527
528 pub fn front_angle(&self) -> f64 {
530 (self.input * self.rack_ratio).clamp(-self.max_wheel_angle, self.max_wheel_angle)
531 }
532
533 pub fn rear_angle(&self) -> f64 {
535 self.front_angle() * self.rear_steer_ratio
536 }
537
538 pub fn ackermann_front_angles(&self) -> (f64, f64) {
540 ackermann_angles(self.front_angle(), self.wheelbase, self.track_width)
541 }
542}
543
544#[derive(Debug, Clone, Serialize, Deserialize)]
550pub struct TcsState {
551 pub active: bool,
553 pub slip_threshold: f64,
555 pub throttle_reduction: f64,
557}
558
559impl TcsState {
560 pub fn new() -> Self {
562 Self {
563 active: false,
564 slip_threshold: 0.2,
565 throttle_reduction: 0.0,
566 }
567 }
568
569 pub fn update(&mut self, slip_ratios: &[f64]) -> f64 {
571 let max_slip = slip_ratios.iter().cloned().fold(0.0_f64, f64::max);
572 if max_slip > self.slip_threshold {
573 self.active = true;
574 self.throttle_reduction =
575 ((max_slip - self.slip_threshold) / self.slip_threshold).min(1.0);
576 } else {
577 self.active = false;
578 self.throttle_reduction = 0.0;
579 }
580 1.0 - self.throttle_reduction
581 }
582}
583
584impl Default for TcsState {
585 fn default() -> Self {
586 Self::new()
587 }
588}
589
590#[derive(Debug, Clone, Serialize, Deserialize)]
592pub struct AbsState {
593 pub active: bool,
595 pub slip_threshold: f64,
597 pub brake_modulation: f64,
599 pub phase: u8,
601}
602
603impl AbsState {
604 pub fn new() -> Self {
606 Self {
607 active: false,
608 slip_threshold: -0.15,
609 brake_modulation: 1.0,
610 phase: 0,
611 }
612 }
613
614 pub fn update(&mut self, slip_ratios: &[f64]) -> f64 {
616 let min_slip = slip_ratios.iter().cloned().fold(0.0_f64, f64::min);
617 if min_slip < self.slip_threshold {
618 self.active = true;
619 self.phase = (self.phase + 1) % 3;
621 self.brake_modulation = if self.phase == 2 { 0.5 } else { 1.0 };
622 } else {
623 self.active = false;
624 self.brake_modulation = 1.0;
625 self.phase = 0;
626 }
627 self.brake_modulation
628 }
629}
630
631impl Default for AbsState {
632 fn default() -> Self {
633 Self::new()
634 }
635}
636
637#[derive(Debug, Clone, Serialize, Deserialize)]
639pub struct EscState {
640 pub active: bool,
642 pub yaw_error_threshold: f64,
644 pub brake_torque: f64,
646 pub yaw_error: f64,
648}
649
650impl EscState {
651 pub fn new() -> Self {
653 Self {
654 active: false,
655 yaw_error_threshold: 0.1,
656 brake_torque: 0.0,
657 yaw_error: 0.0,
658 }
659 }
660
661 pub fn update(&mut self, desired_yaw: f64, actual_yaw: f64) {
663 self.yaw_error = desired_yaw - actual_yaw;
664 if self.yaw_error.abs() > self.yaw_error_threshold {
665 self.active = true;
666 self.brake_torque = self.yaw_error.abs() * 500.0; } else {
668 self.active = false;
669 self.brake_torque = 0.0;
670 }
671 }
672}
673
674impl Default for EscState {
675 fn default() -> Self {
676 Self::new()
677 }
678}
679
680#[derive(Debug, Clone, Serialize, Deserialize)]
682pub struct PyStabilityControl {
683 pub tcs: TcsState,
685 pub abs: AbsState,
687 pub esc: EscState,
689 pub enabled: bool,
691}
692
693impl PyStabilityControl {
694 pub fn new() -> Self {
696 Self {
697 tcs: TcsState::new(),
698 abs: AbsState::new(),
699 esc: EscState::new(),
700 enabled: true,
701 }
702 }
703
704 pub fn update(&mut self, slip_ratios: &[f64], desired_yaw: f64, actual_yaw: f64) -> (f64, f64) {
706 if !self.enabled {
707 return (1.0, 1.0);
708 }
709 let throttle_scale = self.tcs.update(slip_ratios);
710 let brake_scale = self.abs.update(slip_ratios);
711 self.esc.update(desired_yaw, actual_yaw);
712 (throttle_scale, brake_scale)
713 }
714}
715
716impl Default for PyStabilityControl {
717 fn default() -> Self {
718 Self::new()
719 }
720}
721
722#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
728pub enum SuspensionKind {
729 MacPherson,
731 DoubleWishbone,
733 MultiLink,
735 SpringDamper,
737}
738
739#[derive(Debug, Clone, Serialize, Deserialize)]
741pub struct PySuspension {
742 pub kind: SuspensionKind,
744 pub spring_rate: f64,
746 pub damper_compression: f64,
748 pub damper_rebound: f64,
750 pub arb_stiffness: f64,
752 pub bump_stop_rate: f64,
754 pub bump_stop_clearance: f64,
756 pub displacement: f64,
758 pub velocity: f64,
760 pub max_compression: f64,
762 pub max_droop: f64,
764}
765
766impl PySuspension {
767 pub fn double_wishbone() -> Self {
769 Self {
770 kind: SuspensionKind::DoubleWishbone,
771 spring_rate: 30000.0,
772 damper_compression: 2000.0,
773 damper_rebound: 3000.0,
774 arb_stiffness: 5000.0,
775 bump_stop_rate: 100000.0,
776 bump_stop_clearance: 0.02,
777 displacement: 0.0,
778 velocity: 0.0,
779 max_compression: 0.08,
780 max_droop: 0.06,
781 }
782 }
783
784 pub fn macpherson() -> Self {
786 Self {
787 kind: SuspensionKind::MacPherson,
788 spring_rate: 25000.0,
789 damper_compression: 1500.0,
790 damper_rebound: 2500.0,
791 arb_stiffness: 3000.0,
792 bump_stop_rate: 80000.0,
793 bump_stop_clearance: 0.02,
794 displacement: 0.0,
795 velocity: 0.0,
796 max_compression: 0.07,
797 max_droop: 0.05,
798 }
799 }
800
801 pub fn force(&self) -> f64 {
803 let spring_force = -self.spring_rate * self.displacement;
804 let damper_force = if self.velocity > 0.0 {
805 -self.damper_compression * self.velocity
806 } else {
807 -self.damper_rebound * self.velocity
808 };
809 let bump_force = if self.displacement > self.max_compression - self.bump_stop_clearance {
810 let excess = self.displacement - (self.max_compression - self.bump_stop_clearance);
811 -self.bump_stop_rate * excess
812 } else {
813 0.0
814 };
815 spring_force + damper_force + bump_force
816 }
817
818 pub fn step(&mut self, external_displacement: f64, dt: f64) {
820 let prev = self.displacement;
821 self.displacement = external_displacement.clamp(-self.max_droop, self.max_compression);
822 self.velocity = (self.displacement - prev) / dt.max(1e-9);
823 }
824}
825
826#[derive(Debug, Clone, Serialize, Deserialize)]
832pub struct PyWheelDynamics {
833 pub omega: f64,
835 pub inertia: f64,
837 pub radius: f64,
839 pub slip_ratio: f64,
841 pub slip_angle: f64,
843 pub camber: f64,
845 pub toe: f64,
847 pub fx: f64,
849 pub fy: f64,
851 pub fz: f64,
853 pub grounded: bool,
855}
856
857impl PyWheelDynamics {
858 pub fn new(inertia: f64, radius: f64) -> Self {
860 Self {
861 omega: 0.0,
862 inertia,
863 radius,
864 slip_ratio: 0.0,
865 slip_angle: 0.0,
866 camber: 0.0,
867 toe: 0.0,
868 fx: 0.0,
869 fy: 0.0,
870 fz: 0.0,
871 grounded: false,
872 }
873 }
874
875 pub fn step_omega(&mut self, drive_torque: f64, brake_torque: f64, dt: f64) {
877 let road_reaction = -self.fx * self.radius;
879 let net_torque = drive_torque - brake_torque + road_reaction;
880 self.omega += net_torque / self.inertia * dt;
881 if !self.grounded {
882 self.omega *= (1.0 - 0.1 * dt).max(0.0);
884 }
885 }
886
887 pub fn peripheral_speed(&self) -> f64 {
889 self.omega * self.radius
890 }
891}
892
893#[derive(Debug, Clone, Serialize, Deserialize)]
899pub struct PyVehicle {
900 pub mass: f64,
902 pub inertia: [f64; 3],
904 pub cog_height: f64,
906 pub position: [f64; 3],
908 pub velocity: [f64; 3],
910 pub orientation: [f64; 3],
912 pub angular_velocity: [f64; 3],
914 pub wheels: [PyWheelDynamics; 4],
916 pub wheel_positions: [[f64; 3]; 4],
918 pub suspensions: [PySuspension; 4],
920 pub tires: [PyTireModel; 4],
922 pub drivetrain: PyDrivetrain,
924 pub steering: PySteering,
926 pub stability: PyStabilityControl,
928 pub gear_display: u32,
930}
931
932impl PyVehicle {
933 pub fn sedan() -> Self {
935 let wheel = PyWheelDynamics::new(1.5, 0.32);
936 let suspension = PySuspension::macpherson();
937 let tire = PyTireModel::road_pacejka(0.32);
938 Self {
939 mass: 1500.0,
940 inertia: [2000.0, 2500.0, 3000.0],
941 cog_height: 0.5,
942 position: [0.0; 3],
943 velocity: [0.0; 3],
944 orientation: [0.0; 3],
945 angular_velocity: [0.0; 3],
946 wheels: [wheel.clone(), wheel.clone(), wheel.clone(), wheel.clone()],
947 wheel_positions: [
948 [-0.75, -0.5, 1.3],
949 [0.75, -0.5, 1.3],
950 [-0.75, -0.5, -1.2],
951 [0.75, -0.5, -1.2],
952 ],
953 suspensions: [
954 suspension.clone(),
955 suspension.clone(),
956 suspension.clone(),
957 suspension.clone(),
958 ],
959 tires: [tire.clone(), tire.clone(), tire.clone(), tire.clone()],
960 drivetrain: PyDrivetrain::six_speed(300.0),
961 steering: PySteering::new(2.6, 1.5),
962 stability: PyStabilityControl::new(),
963 gear_display: 1,
964 }
965 }
966
967 pub fn racing_car() -> Self {
969 let wheel = PyWheelDynamics::new(1.0, 0.30);
970 let suspension = PySuspension::double_wishbone();
971 let tire = PyTireModel::slick_pacejka(0.30);
972 Self {
973 mass: 700.0,
974 inertia: [800.0, 1000.0, 1200.0],
975 cog_height: 0.3,
976 position: [0.0; 3],
977 velocity: [0.0; 3],
978 orientation: [0.0; 3],
979 angular_velocity: [0.0; 3],
980 wheels: [wheel.clone(), wheel.clone(), wheel.clone(), wheel.clone()],
981 wheel_positions: [
982 [-0.7, -0.4, 1.6],
983 [0.7, -0.4, 1.6],
984 [-0.7, -0.4, -1.6],
985 [0.7, -0.4, -1.6],
986 ],
987 suspensions: [
988 suspension.clone(),
989 suspension.clone(),
990 suspension.clone(),
991 suspension.clone(),
992 ],
993 tires: [tire.clone(), tire.clone(), tire.clone(), tire.clone()],
994 drivetrain: PyDrivetrain::six_speed(500.0),
995 steering: PySteering::new(2.8, 1.4),
996 stability: PyStabilityControl::new(),
997 gear_display: 1,
998 }
999 }
1000
1001 pub fn forward_speed(&self) -> f64 {
1003 let yaw = self.orientation[2];
1005 self.velocity[0] * yaw.cos() + self.velocity[2] * yaw.sin()
1006 }
1007
1008 pub fn step(&mut self, dt: f64, throttle: f64, brake: f64, steer: f64) {
1015 self.steering.input = steer.clamp(-1.0, 1.0);
1017 let front_angle = self.steering.front_angle();
1018
1019 let v = self.forward_speed().abs().max(0.01);
1021 let wheel_speeds: [f64; 4] = core::array::from_fn(|i| self.wheels[i].peripheral_speed());
1022 let slip_ratios: Vec<f64> = wheel_speeds
1023 .iter()
1024 .map(|&ws| compute_slip_ratio(ws, v))
1025 .collect();
1026
1027 let desired_yaw = v / 2.6 * front_angle;
1029 let actual_yaw = self.angular_velocity[1];
1030 let (throttle_scale, brake_scale) =
1031 self.stability.update(&slip_ratios, desired_yaw, actual_yaw);
1032 let effective_throttle = throttle * throttle_scale;
1033 let effective_brake = brake * brake_scale;
1034
1035 let drive_torque_total = self.drivetrain.wheel_torque(effective_throttle);
1037 let drive_torque_per_wheel = drive_torque_total / 2.0; let max_brake_torque = 2000.0;
1041 let brake_torque = effective_brake * max_brake_torque;
1042
1043 for i in [2, 3] {
1045 let slip_angle = self.wheels[i].slip_angle;
1046 let kappa = self.wheels[i].slip_ratio;
1047 let fz = self.mass * 9.81 * 0.25; let (fx, fy) = self.tires[i].compute_forces(slip_angle, kappa, fz);
1049 self.wheels[i].fx = fx;
1050 self.wheels[i].fy = fy;
1051 self.wheels[i].fz = fz;
1052 self.wheels[i].grounded = true;
1053 self.wheels[i].step_omega(drive_torque_per_wheel, brake_torque, dt);
1054 }
1055 for i in [0, 1] {
1057 self.wheels[i].slip_angle = -front_angle;
1058 let fz = self.mass * 9.81 * 0.25;
1059 let (fx, fy) = self.tires[i].compute_forces(self.wheels[i].slip_angle, 0.0, fz);
1060 self.wheels[i].fx = fx;
1061 self.wheels[i].fy = fy;
1062 self.wheels[i].fz = fz;
1063 self.wheels[i].grounded = true;
1064 self.wheels[i].step_omega(0.0, brake_torque, dt);
1065 }
1066
1067 let total_fx: f64 = self.wheels.iter().map(|w| w.fx).sum();
1069 let total_fy: f64 = self.wheels.iter().map(|w| w.fy).sum();
1070
1071 let ax = total_fx / self.mass;
1073 let az = total_fy / self.mass - 9.81 * self.orientation[0].sin();
1074 self.velocity[0] += ax * dt;
1075 self.velocity[2] += az * dt;
1076
1077 let drag = 0.3 * self.velocity[0] * self.velocity[0].abs();
1079 self.velocity[0] -= drag / self.mass * dt;
1080
1081 self.position[0] += self.velocity[0] * dt;
1082 self.position[1] += self.velocity[1] * dt;
1083 self.position[2] += self.velocity[2] * dt;
1084
1085 let avg_rear_omega = (self.wheels[2].omega + self.wheels[3].omega) * 0.5;
1087 self.drivetrain.update_rpm(avg_rear_omega);
1088 self.gear_display = (self.drivetrain.current_gear + 1) as u32;
1089 }
1090}
1091
1092#[derive(Debug, Clone, Serialize, Deserialize)]
1098pub struct TelemetrySample {
1099 pub time: f64,
1101 pub speed: f64,
1103 pub g_lon: f64,
1105 pub g_lat: f64,
1107 pub slip_angles: [f64; 4],
1109 pub tire_temps: [f64; 4],
1111 pub gear: u32,
1113 pub rpm: f64,
1115 pub throttle: f64,
1117 pub brake: f64,
1119 pub steer: f64,
1121}
1122
1123#[derive(Debug, Clone, Serialize, Deserialize)]
1125pub struct LapStats {
1126 pub lap_time: f64,
1128 pub max_speed: f64,
1130 pub avg_speed: f64,
1132 pub max_g_lat: f64,
1134 pub max_g_lon_brake: f64,
1136 pub max_g_lon_accel: f64,
1138 pub sample_count: usize,
1140}
1141
1142#[derive(Debug, Clone, Serialize, Deserialize)]
1144pub struct PyTelemetry {
1145 pub samples: Vec<TelemetrySample>,
1147 pub max_samples: usize,
1149 pub recording: bool,
1151}
1152
1153impl PyTelemetry {
1154 pub fn new(max_samples: usize) -> Self {
1156 Self {
1157 samples: Vec::new(),
1158 max_samples,
1159 recording: true,
1160 }
1161 }
1162
1163 pub fn record(&mut self, sample: TelemetrySample) {
1165 if !self.recording {
1166 return;
1167 }
1168 if self.samples.len() >= self.max_samples {
1169 self.samples.remove(0);
1170 }
1171 self.samples.push(sample);
1172 }
1173
1174 pub fn lap_stats(&self) -> Option<LapStats> {
1176 if self.samples.is_empty() {
1177 return None;
1178 }
1179 let n = self.samples.len();
1180 let total_time = self
1181 .samples
1182 .last()
1183 .expect("collection should not be empty")
1184 .time
1185 - self.samples[0].time;
1186 let max_speed = self.samples.iter().map(|s| s.speed).fold(0.0_f64, f64::max);
1187 let avg_speed = self.samples.iter().map(|s| s.speed).sum::<f64>() / n as f64;
1188 let max_g_lat = self
1189 .samples
1190 .iter()
1191 .map(|s| s.g_lat.abs())
1192 .fold(0.0_f64, f64::max);
1193 let max_g_lon_brake = self
1194 .samples
1195 .iter()
1196 .map(|s| (-s.g_lon).max(0.0))
1197 .fold(0.0_f64, f64::max);
1198 let max_g_lon_accel = self
1199 .samples
1200 .iter()
1201 .map(|s| s.g_lon.max(0.0))
1202 .fold(0.0_f64, f64::max);
1203 Some(LapStats {
1204 lap_time: total_time,
1205 max_speed,
1206 avg_speed,
1207 max_g_lat,
1208 max_g_lon_brake,
1209 max_g_lon_accel,
1210 sample_count: n,
1211 })
1212 }
1213
1214 pub fn clear(&mut self) {
1216 self.samples.clear();
1217 }
1218}
1219
1220#[derive(Debug, Clone, Serialize, Deserialize)]
1226pub struct TrackPoint {
1227 pub x: f64,
1229 pub y: f64,
1231 pub half_width: f64,
1233}
1234
1235impl TrackPoint {
1236 pub fn new(x: f64, y: f64, half_width: f64) -> Self {
1238 Self { x, y, half_width }
1239 }
1240}
1241
1242#[derive(Debug, Clone, Serialize, Deserialize)]
1244pub struct PyRacingLine {
1245 pub centerline: Vec<TrackPoint>,
1247 pub racing_line: Vec<[f64; 2]>,
1249 pub sector_boundaries: Vec<usize>,
1251 pub sector_times: Vec<f64>,
1253 pub track_length: f64,
1255}
1256
1257impl PyRacingLine {
1258 pub fn new(centerline: Vec<TrackPoint>) -> Self {
1260 let n = centerline.len();
1261 let racing_line = centerline.iter().map(|p| [p.x, p.y]).collect();
1263 let track_length = Self::compute_length(¢erline);
1264 Self {
1265 centerline,
1266 racing_line,
1267 sector_boundaries: if n >= 3 {
1268 vec![0, n / 3, 2 * n / 3, n - 1]
1269 } else {
1270 vec![0, n.max(1) - 1]
1271 },
1272 sector_times: Vec::new(),
1273 track_length,
1274 }
1275 }
1276
1277 fn compute_length(pts: &[TrackPoint]) -> f64 {
1278 let mut len = 0.0;
1279 for i in 1..pts.len() {
1280 let dx = pts[i].x - pts[i - 1].x;
1281 let dy = pts[i].y - pts[i - 1].y;
1282 len += (dx * dx + dy * dy).sqrt();
1283 }
1284 len
1285 }
1286
1287 pub fn curvature_at(&self, i: usize) -> f64 {
1289 let n = self.racing_line.len();
1290 if n < 3 || i == 0 || i >= n - 1 {
1291 return 0.0;
1292 }
1293 let p0 = self.racing_line[i - 1];
1294 let p1 = self.racing_line[i];
1295 let p2 = self.racing_line[i + 1];
1296 let a = ((p1[0] - p0[0]).powi(2) + (p1[1] - p0[1]).powi(2)).sqrt();
1298 let b = ((p2[0] - p1[0]).powi(2) + (p2[1] - p1[1]).powi(2)).sqrt();
1299 let c = ((p2[0] - p0[0]).powi(2) + (p2[1] - p0[1]).powi(2)).sqrt();
1300 let area2 = ((p1[0] - p0[0]) * (p2[1] - p0[1]) - (p1[1] - p0[1]) * (p2[0] - p0[0])).abs();
1301 if a * b * c < 1e-12 {
1302 0.0
1303 } else {
1304 area2 / (a * b * c)
1305 }
1306 }
1307
1308 pub fn optimize_step(&mut self, alpha: f64) {
1310 let n = self.racing_line.len();
1311 if n < 3 {
1312 return;
1313 }
1314 let mut new_line = self.racing_line.clone();
1315 for i in 1..n - 1 {
1316 let p0 = self.racing_line[i - 1];
1317 let p1 = self.racing_line[i];
1318 let p2 = self.racing_line[i + 1];
1319 let mid = [(p0[0] + p2[0]) * 0.5, (p0[1] + p2[1]) * 0.5];
1321 let new_x = p1[0] + alpha * (mid[0] - p1[0]);
1322 let new_y = p1[1] + alpha * (mid[1] - p1[1]);
1323 let cl = &self.centerline[i];
1325 let dx = new_x - cl.x;
1326 let dy = new_y - cl.y;
1327 let dist = (dx * dx + dy * dy).sqrt();
1328 if dist <= cl.half_width {
1329 new_line[i] = [new_x, new_y];
1330 } else if dist > 1e-12 {
1331 new_line[i] = [
1332 cl.x + dx / dist * cl.half_width,
1333 cl.y + dy / dist * cl.half_width,
1334 ];
1335 }
1336 }
1337 self.racing_line = new_line;
1338 }
1339
1340 pub fn optimize(&mut self, n_iter: u32, alpha: f64) {
1342 for _ in 0..n_iter {
1343 self.optimize_step(alpha);
1344 }
1345 }
1346
1347 pub fn total_curvature(&self) -> f64 {
1349 (1..self.racing_line.len() - 1)
1350 .map(|i| self.curvature_at(i))
1351 .sum()
1352 }
1353}
1354
1355#[cfg(test)]
1360mod tests {
1361 use super::*;
1362
1363 #[test]
1364 fn test_pacejka_fy_zero_slip() {
1365 let fy = pacejka_fy(0.0, 10.0, 1.9, 1.0, 0.97);
1366 assert!(fy.abs() < 1e-9);
1367 }
1368
1369 #[test]
1370 fn test_pacejka_fy_nonzero() {
1371 let fy = pacejka_fy(0.1, 10.0, 1.9, 1.0, 0.97);
1372 assert!(fy.abs() > 0.0);
1373 }
1374
1375 #[test]
1376 fn test_pacejka_fx_peak() {
1377 let fx = pacejka_fx(0.2, 11.0, 1.65, 1.0, 0.0);
1378 assert!(fx.abs() > 0.0);
1379 }
1380
1381 #[test]
1382 fn test_compute_slip_angle() {
1383 let alpha = compute_slip_angle(1.0, 10.0);
1384 assert!(alpha.abs() < 0.2);
1385 }
1386
1387 #[test]
1388 fn test_compute_slip_ratio_no_slip() {
1389 let kappa = compute_slip_ratio(10.0, 10.0);
1390 assert!(kappa.abs() < 1e-9);
1391 }
1392
1393 #[test]
1394 fn test_compute_slip_ratio_drive_slip() {
1395 let kappa = compute_slip_ratio(12.0, 10.0);
1396 assert!(kappa > 0.0);
1397 }
1398
1399 #[test]
1400 fn test_ackermann_straight() {
1401 let (a, b) = ackermann_angles(0.0, 2.5, 1.5);
1402 assert!(a.abs() < 1e-9);
1403 assert!(b.abs() < 1e-9);
1404 }
1405
1406 #[test]
1407 fn test_ackermann_turn() {
1408 let (inner, outer) = ackermann_angles(0.3, 2.5, 1.5);
1409 assert!(inner.abs() > outer.abs());
1410 }
1411
1412 #[test]
1413 fn test_tire_model_forces() {
1414 let tm = PyTireModel::road_pacejka(0.32);
1415 let (fx, fy) = tm.compute_forces(0.1, 0.05, 3000.0);
1416 let _ = fx;
1417 assert!(fy.abs() > 0.0);
1418 }
1419
1420 #[test]
1421 fn test_tire_thermal_update() {
1422 let mut tt = PyTireThermal::new();
1423 let grip0 = tt.grip_scale();
1424 tt.update(5000.0, 10.0);
1425 assert!(tt.surface_temp > 25.0);
1426 assert!(tt.grip_scale() >= 0.0 && tt.grip_scale() <= 1.0);
1428 let _ = grip0;
1429 }
1430
1431 #[test]
1432 fn test_tire_thermal_optimal() {
1433 let mut tt = PyTireThermal::new();
1434 tt.surface_temp = 90.0;
1435 assert!((tt.grip_scale() - 1.0).abs() < 1e-9);
1436 }
1437
1438 #[test]
1439 fn test_engine_curve_flat() {
1440 let ec = EngineCurve::flat(300.0, 7000.0);
1441 let t = ec.torque_at(3500.0);
1442 assert!(t > 0.0);
1443 assert!(t <= 300.0 * 1.01);
1444 }
1445
1446 #[test]
1447 fn test_drivetrain_upshift() {
1448 let mut dt = PyDrivetrain::six_speed(300.0);
1449 dt.upshift();
1450 assert_eq!(dt.current_gear, 2);
1451 }
1452
1453 #[test]
1454 fn test_drivetrain_downshift() {
1455 let mut dt = PyDrivetrain::six_speed(300.0);
1456 dt.downshift();
1457 assert_eq!(dt.current_gear, 0);
1458 }
1459
1460 #[test]
1461 fn test_drivetrain_wheel_torque() {
1462 let dt = PyDrivetrain::six_speed(300.0);
1463 let t = dt.wheel_torque(1.0);
1464 assert!(t > 0.0);
1465 }
1466
1467 #[test]
1468 fn test_steering_angles() {
1469 let mut steer = PySteering::new(2.6, 1.5);
1470 steer.input = 1.0;
1471 let fa = steer.front_angle();
1472 assert!(fa > 0.0);
1473 assert!(fa <= steer.max_wheel_angle);
1474 }
1475
1476 #[test]
1477 fn test_stability_tcs() {
1478 let mut sc = PyStabilityControl::new();
1479 let (ts, _) = sc.update(&[0.5, 0.5, 0.5, 0.5], 0.0, 0.0);
1480 assert!(ts < 1.0);
1481 }
1482
1483 #[test]
1484 fn test_stability_disabled() {
1485 let mut sc = PyStabilityControl::new();
1486 sc.enabled = false;
1487 let (ts, bs) = sc.update(&[2.0, 2.0, 2.0, 2.0], 0.0, 0.0);
1488 assert!((ts - 1.0).abs() < 1e-9);
1489 assert!((bs - 1.0).abs() < 1e-9);
1490 }
1491
1492 #[test]
1493 fn test_suspension_force() {
1494 let mut s = PySuspension::double_wishbone();
1495 s.step(0.03, 0.01);
1496 let f = s.force();
1497 assert!(f < 0.0); }
1499
1500 #[test]
1501 fn test_suspension_bump_stop() {
1502 let mut s = PySuspension::double_wishbone();
1503 s.step(s.max_compression, 0.01);
1504 let f = s.force();
1505 assert!(f < 0.0);
1506 }
1507
1508 #[test]
1509 fn test_wheel_dynamics_step() {
1510 let mut w = PyWheelDynamics::new(1.5, 0.32);
1511 w.fx = 100.0;
1512 w.grounded = true;
1513 w.step_omega(200.0, 0.0, 0.01);
1514 assert!(w.omega > 0.0);
1515 }
1516
1517 #[test]
1518 fn test_vehicle_sedan_step() {
1519 let mut v = PyVehicle::sedan();
1520 v.step(0.016, 0.5, 0.0, 0.0);
1521 let speed = v.forward_speed();
1523 let _ = speed;
1524 assert!(v.drivetrain.engine_rpm > 0.0);
1525 }
1526
1527 #[test]
1528 fn test_vehicle_racing_step() {
1529 let mut v = PyVehicle::racing_car();
1530 v.step(0.016, 1.0, 0.0, 0.1);
1531 assert!(v.gear_display >= 1);
1532 }
1533
1534 #[test]
1535 fn test_telemetry_record() {
1536 let mut tel = PyTelemetry::new(100);
1537 let sample = TelemetrySample {
1538 time: 0.0,
1539 speed: 50.0,
1540 g_lon: 0.5,
1541 g_lat: 0.3,
1542 slip_angles: [0.0; 4],
1543 tire_temps: [80.0; 4],
1544 gear: 3,
1545 rpm: 4000.0,
1546 throttle: 0.8,
1547 brake: 0.0,
1548 steer: 0.1,
1549 };
1550 tel.record(sample);
1551 assert_eq!(tel.samples.len(), 1);
1552 }
1553
1554 #[test]
1555 fn test_telemetry_lap_stats() {
1556 let mut tel = PyTelemetry::new(100);
1557 for i in 0..10 {
1558 let sample = TelemetrySample {
1559 time: i as f64 * 0.1,
1560 speed: 50.0 + i as f64,
1561 g_lon: 0.1,
1562 g_lat: 0.2,
1563 slip_angles: [0.0; 4],
1564 tire_temps: [80.0; 4],
1565 gear: 3,
1566 rpm: 4000.0,
1567 throttle: 0.8,
1568 brake: 0.0,
1569 steer: 0.0,
1570 };
1571 tel.record(sample);
1572 }
1573 let stats = tel.lap_stats().unwrap();
1574 assert!(stats.max_speed > 50.0);
1575 assert!(stats.sample_count == 10);
1576 }
1577
1578 #[test]
1579 fn test_racing_line_curvature() {
1580 let pts: Vec<TrackPoint> = (0..10)
1581 .map(|i| TrackPoint::new(i as f64, 0.0, 3.0))
1582 .collect();
1583 let rl = PyRacingLine::new(pts);
1584 let k = rl.curvature_at(5);
1586 assert!(k < 1e-9);
1587 }
1588
1589 #[test]
1590 fn test_racing_line_optimize() {
1591 let pts: Vec<TrackPoint> = (0..20)
1592 .map(|i| {
1593 let t = i as f64 / 20.0 * 2.0 * std::f64::consts::PI;
1594 TrackPoint::new(t.cos() * 10.0, t.sin() * 10.0, 2.0)
1595 })
1596 .collect();
1597 let mut rl = PyRacingLine::new(pts);
1598 let k0 = rl.total_curvature();
1599 rl.optimize(10, 0.1);
1600 let k1 = rl.total_curvature();
1601 assert!(k1 <= k0 * 1.5 + 0.1);
1603 }
1604
1605 #[test]
1606 fn test_fiala_tire_linear() {
1607 let fp = FialaParams::new(50000.0, 1.0);
1608 let fy = fp.lateral_force(0.05, 4000.0);
1609 let expected = -50000.0 * 0.05;
1610 assert!((fy - expected).abs() < 1.0);
1611 }
1612
1613 #[test]
1614 fn test_fiala_tire_saturated() {
1615 let fp = FialaParams::new(50000.0, 1.0);
1616 let fy = fp.lateral_force(1.0, 4000.0);
1617 assert!(fy.abs() <= 4000.0 + 1.0);
1618 }
1619}