1use rust_robotics_core::{ControlInput, Path2D, PathTracker, Point2D, State2D};
12use std::f64::consts::PI;
13
14#[derive(Debug, Clone, Copy)]
16pub struct VehicleState {
17 pub x: f64,
18 pub y: f64,
19 pub yaw: f64,
20 pub v: f64,
21 pub wheelbase: f64,
22}
23
24impl VehicleState {
25 pub fn new(x: f64, y: f64, yaw: f64, v: f64, wheelbase: f64) -> Self {
26 VehicleState {
27 x,
28 y,
29 yaw,
30 v,
31 wheelbase,
32 }
33 }
34
35 pub fn update(&mut self, a: f64, delta: f64, dt: f64) {
37 self.x += self.v * self.yaw.cos() * dt;
38 self.y += self.v * self.yaw.sin() * dt;
39 self.yaw += self.v / self.wheelbase * delta.tan() * dt;
40 self.v += a * dt;
41 }
42
43 pub fn rear_axle(&self) -> (f64, f64) {
45 (self.x, self.y)
46 }
47
48 pub fn to_state2d(&self) -> State2D {
49 State2D::new(self.x, self.y, self.yaw, self.v)
50 }
51}
52
53impl From<State2D> for VehicleState {
54 fn from(s: State2D) -> Self {
55 VehicleState::new(s.x, s.y, s.yaw, s.v, 2.9) }
57}
58
59#[derive(Debug, Clone)]
61pub struct RearWheelFeedbackConfig {
62 pub kth: f64,
64 pub ke: f64,
66 pub wheelbase: f64,
68 pub kp: f64,
70 pub goal_threshold: f64,
72 pub max_steer: f64,
74}
75
76impl Default for RearWheelFeedbackConfig {
77 fn default() -> Self {
78 Self {
79 kth: 1.0,
80 ke: 0.5,
81 wheelbase: 2.9,
82 kp: 1.0,
83 goal_threshold: 0.5,
84 max_steer: 45.0_f64.to_radians(),
85 }
86 }
87}
88
89pub struct RearWheelFeedbackController {
91 config: RearWheelFeedbackConfig,
92 path: Path2D,
93 path_yaw: Vec<f64>,
94 path_curvature: Vec<f64>,
95 last_target_idx: usize,
96}
97
98impl RearWheelFeedbackController {
99 pub fn new(config: RearWheelFeedbackConfig) -> Self {
101 RearWheelFeedbackController {
102 config,
103 path: Path2D::new(),
104 path_yaw: Vec::new(),
105 path_curvature: Vec::new(),
106 last_target_idx: 0,
107 }
108 }
109
110 pub fn with_params(kth: f64, ke: f64, wheelbase: f64) -> Self {
112 let config = RearWheelFeedbackConfig {
113 kth,
114 ke,
115 wheelbase,
116 ..Default::default()
117 };
118 Self::new(config)
119 }
120
121 pub fn set_path(&mut self, path: Path2D) {
123 self.path_yaw = self.compute_path_yaw(&path);
124 self.path_curvature = self.compute_path_curvature(&path);
125 self.path = path;
126 self.last_target_idx = 0;
127 }
128
129 pub fn set_path_with_info(&mut self, path: Path2D, yaw: Vec<f64>, curvature: Vec<f64>) {
131 self.path = path;
132 self.path_yaw = yaw;
133 self.path_curvature = curvature;
134 self.last_target_idx = 0;
135 }
136
137 pub fn get_path(&self) -> &Path2D {
139 &self.path
140 }
141
142 fn compute_path_yaw(&self, path: &Path2D) -> Vec<f64> {
144 path.yaw_profile()
145 }
146
147 fn compute_path_curvature(&self, path: &Path2D) -> Vec<f64> {
149 let n = path.len();
150 if n < 3 {
151 return vec![0.0; n];
152 }
153
154 let mut curvature = Vec::with_capacity(n);
155
156 curvature.push(0.0);
158
159 for i in 1..n - 1 {
161 let p0 = &path.points[i - 1];
162 let p1 = &path.points[i];
163 let p2 = &path.points[i + 1];
164
165 let dx1 = p1.x - p0.x;
166 let dy1 = p1.y - p0.y;
167 let dx2 = p2.x - p1.x;
168 let dy2 = p2.y - p1.y;
169
170 let ds1 = (dx1 * dx1 + dy1 * dy1).sqrt();
171 let ds2 = (dx2 * dx2 + dy2 * dy2).sqrt();
172
173 if ds1 > 1e-6 && ds2 > 1e-6 {
174 let yaw1 = dy1.atan2(dx1);
175 let yaw2 = dy2.atan2(dx2);
176 let dyaw = Self::normalize_angle(yaw2 - yaw1);
177 let ds = (ds1 + ds2) / 2.0;
178 curvature.push(dyaw / ds);
179 } else {
180 curvature.push(0.0);
181 }
182 }
183
184 curvature.push(*curvature.last().unwrap_or(&0.0));
186
187 curvature
188 }
189
190 fn normalize_angle(mut angle: f64) -> f64 {
192 while angle > PI {
193 angle -= 2.0 * PI;
194 }
195 while angle < -PI {
196 angle += 2.0 * PI;
197 }
198 angle
199 }
200
201 fn calc_target_index(&self, state: &VehicleState) -> (usize, f64) {
203 let (rx, ry) = state.rear_axle();
204 let query = Point2D::new(rx, ry);
205 let min_idx = self
206 .path
207 .nearest_point_index_from(query, self.last_target_idx)
208 .unwrap_or(0);
209
210 let target_point = &self.path.points[min_idx];
212 let dx = rx - target_point.x;
213 let dy = ry - target_point.y;
214
215 let path_yaw = if min_idx < self.path_yaw.len() {
217 self.path_yaw[min_idx]
218 } else {
219 0.0
220 };
221
222 let error = -path_yaw.sin() * dx + path_yaw.cos() * dy;
224
225 (min_idx, error)
226 }
227
228 pub fn compute_steering(&mut self, state: &VehicleState) -> f64 {
234 if self.path.is_empty() {
235 return 0.0;
236 }
237
238 let (mut target_idx, e) = self.calc_target_index(state);
239
240 if self.last_target_idx >= target_idx {
242 target_idx = self.last_target_idx;
243 }
244 self.last_target_idx = target_idx;
245
246 let yaw_ref = if target_idx < self.path_yaw.len() {
248 self.path_yaw[target_idx]
249 } else {
250 *self.path_yaw.last().unwrap_or(&0.0)
251 };
252
253 let k = if target_idx < self.path_curvature.len() {
254 self.path_curvature[target_idx]
255 } else {
256 *self.path_curvature.last().unwrap_or(&0.0)
257 };
258
259 let th_e = Self::normalize_angle(state.yaw - yaw_ref);
261
262 let v = state.v;
264 let v_abs = v.abs().max(0.1); let th_e_safe = if th_e.abs() < 1e-6 {
268 1e-6 * th_e.signum().max(1.0)
269 } else {
270 th_e
271 };
272
273 let denom = 1.0 - k * e;
275 let denom_safe = if denom.abs() < 0.01 {
276 0.01 * denom.signum().max(1.0)
277 } else {
278 denom
279 };
280
281 let omega = v * k * th_e.cos() / denom_safe
282 - self.config.kth * v_abs * th_e
283 - self.config.ke * v * th_e.sin() * e / th_e_safe;
284
285 let delta = if v.abs() > 0.01 {
287 (self.config.wheelbase * omega / v).atan()
288 } else {
289 0.0
290 };
291
292 delta.clamp(-self.config.max_steer, self.config.max_steer)
294 }
295
296 pub fn compute_acceleration(&self, target_speed: f64, current_speed: f64) -> f64 {
298 self.config.kp * (target_speed - current_speed)
299 }
300
301 pub fn is_goal_reached_vehicle(&self, state: &VehicleState) -> bool {
303 if let Some(goal) = self.path.points.last() {
304 let dx = state.x - goal.x;
305 let dy = state.y - goal.y;
306 (dx * dx + dy * dy).sqrt() < self.config.goal_threshold
307 } else {
308 true
309 }
310 }
311
312 pub fn planning(
314 &mut self,
315 waypoints: Vec<(f64, f64)>,
316 target_speed: f64,
317 ds: f64,
318 ) -> Option<Vec<(f64, f64)>> {
319 if waypoints.len() < 2 {
320 return None;
321 }
322
323 let ax: Vec<f64> = waypoints.iter().map(|p| p.0).collect();
325 let ay: Vec<f64> = waypoints.iter().map(|p| p.1).collect();
326 let (cx, cy, cyaw, ck, _) = calc_spline_course(&ax, &ay, ds);
327
328 let path = Path2D::from_points(
330 cx.iter()
331 .zip(cy.iter())
332 .map(|(&x, &y)| Point2D::new(x, y))
333 .collect(),
334 );
335 self.set_path_with_info(path, cyaw, ck);
336
337 let mut state = VehicleState::new(
339 waypoints[0].0,
340 waypoints[0].1,
341 self.path_yaw.first().copied().unwrap_or(0.0),
342 0.0,
343 self.config.wheelbase,
344 );
345
346 let mut trajectory = vec![(state.x, state.y)];
347 let dt = 0.1;
348 let t_max = 100.0;
349 let mut time = 0.0;
350
351 while time < t_max {
352 let ai = self.compute_acceleration(target_speed, state.v);
353 let di = self.compute_steering(&state);
354 state.update(ai, di, dt);
355 time += dt;
356
357 trajectory.push((state.x, state.y));
358
359 if self.is_goal_reached_vehicle(&state) {
360 break;
361 }
362 }
363
364 Some(trajectory)
365 }
366}
367
368impl PathTracker for RearWheelFeedbackController {
369 fn compute_control(&mut self, current_state: &State2D, path: &Path2D) -> ControlInput {
370 if self.path.len() != path.len() {
372 self.set_path(path.clone());
373 }
374
375 let vehicle_state = VehicleState::new(
376 current_state.x,
377 current_state.y,
378 current_state.yaw,
379 current_state.v,
380 self.config.wheelbase,
381 );
382
383 let delta = self.compute_steering(&vehicle_state);
384
385 let target_speed = 5.0; let v = current_state.v + self.compute_acceleration(target_speed, current_state.v) * 0.1;
388
389 let omega = v * delta.tan() / self.config.wheelbase;
391
392 ControlInput::new(v, omega)
393 }
394
395 fn is_goal_reached(&self, current_state: &State2D, goal: Point2D) -> bool {
396 let dx = current_state.x - goal.x;
397 let dy = current_state.y - goal.y;
398 (dx * dx + dy * dy).sqrt() < self.config.goal_threshold
399 }
400}
401
402fn calc_spline_course(x: &[f64], y: &[f64], ds: f64) -> SplineCourse {
405 let sp = CubicSpline2D::new(x, y);
406 let mut s = 0.0;
407 let mut course_x = Vec::new();
408 let mut course_y = Vec::new();
409 let mut course_yaw = Vec::new();
410 let mut course_k = Vec::new();
411 let mut course_s = Vec::new();
412
413 let s_max = *sp.s.last().unwrap() - ds;
414 while s < s_max {
415 let (ix, iy) = sp.calc_position(s);
416 let iyaw = sp.calc_yaw(s);
417 let ik = sp.calc_curvature(s);
418 course_x.push(ix);
419 course_y.push(iy);
420 course_yaw.push(iyaw);
421 course_k.push(ik);
422 course_s.push(s);
423 s += ds;
424 }
425
426 (course_x, course_y, course_yaw, course_k, course_s)
427}
428
429struct CubicSpline {
430 a: Vec<f64>,
431 b: Vec<f64>,
432 c: Vec<f64>,
433 d: Vec<f64>,
434 x: Vec<f64>,
435}
436
437type SplineCourse = (Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>);
438
439impl CubicSpline {
440 fn new(x: &[f64], y: &[f64]) -> Self {
441 let n = x.len();
442 let mut h = vec![0.0; n - 1];
443 for i in 0..n - 1 {
444 h[i] = x[i + 1] - x[i];
445 }
446
447 let mut a = vec![0.0; n];
448 let mut b = vec![0.0; n];
449 let mut c = vec![0.0; n];
450 let mut d = vec![0.0; n];
451
452 a[..n].copy_from_slice(&y[..n]);
453
454 let mut alpha = vec![0.0; n - 1];
455 for i in 1..n - 1 {
456 alpha[i] = 3.0 * (a[i + 1] - a[i]) / h[i] - 3.0 * (a[i] - a[i - 1]) / h[i - 1];
457 }
458
459 let mut l = vec![1.0; n];
460 let mut mu = vec![0.0; n];
461 let mut z = vec![0.0; n];
462
463 for i in 1..n - 1 {
464 l[i] = 2.0 * (x[i + 1] - x[i - 1]) - h[i - 1] * mu[i - 1];
465 mu[i] = h[i] / l[i];
466 z[i] = (alpha[i] - h[i - 1] * z[i - 1]) / l[i];
467 }
468
469 for j in (0..n - 1).rev() {
470 c[j] = z[j] - mu[j] * c[j + 1];
471 b[j] = (a[j + 1] - a[j]) / h[j] - h[j] * (c[j + 1] + 2.0 * c[j]) / 3.0;
472 d[j] = (c[j + 1] - c[j]) / (3.0 * h[j]);
473 }
474
475 CubicSpline {
476 a,
477 b,
478 c,
479 d,
480 x: x.to_vec(),
481 }
482 }
483
484 fn calc(&self, t: f64) -> f64 {
485 if t < self.x[0] {
486 return self.a[0];
487 } else if t > self.x[self.x.len() - 1] {
488 return self.a[self.a.len() - 1];
489 }
490
491 let mut i = self.search_index(t);
492 if i >= self.x.len() - 1 {
493 i = self.x.len() - 2;
494 }
495
496 let dx = t - self.x[i];
497 self.a[i] + self.b[i] * dx + self.c[i] * dx * dx + self.d[i] * dx * dx * dx
498 }
499
500 fn calc_d(&self, t: f64) -> f64 {
501 if t < self.x[0] {
502 return self.b[0];
503 } else if t > self.x[self.x.len() - 1] {
504 return self.b[self.b.len() - 1];
505 }
506
507 let mut i = self.search_index(t);
508 if i >= self.x.len() - 1 {
509 i = self.x.len() - 2;
510 }
511
512 let dx = t - self.x[i];
513 self.b[i] + 2.0 * self.c[i] * dx + 3.0 * self.d[i] * dx * dx
514 }
515
516 fn calc_dd(&self, t: f64) -> f64 {
517 if t < self.x[0] {
518 return 2.0 * self.c[0];
519 } else if t > self.x[self.x.len() - 1] {
520 return 2.0 * self.c[self.c.len() - 1];
521 }
522
523 let mut i = self.search_index(t);
524 if i >= self.x.len() - 1 {
525 i = self.x.len() - 2;
526 }
527
528 let dx = t - self.x[i];
529 2.0 * self.c[i] + 6.0 * self.d[i] * dx
530 }
531
532 fn search_index(&self, x: f64) -> usize {
533 for i in 0..self.x.len() - 1 {
534 if self.x[i] <= x && x <= self.x[i + 1] {
535 return i;
536 }
537 }
538 self.x.len() - 2
539 }
540}
541
542struct CubicSpline2D {
543 s: Vec<f64>,
544 sx: CubicSpline,
545 sy: CubicSpline,
546}
547
548impl CubicSpline2D {
549 fn new(x: &[f64], y: &[f64]) -> Self {
550 let mut s = vec![0.0];
551 for i in 1..x.len() {
552 let dx = x[i] - x[i - 1];
553 let dy = y[i] - y[i - 1];
554 s.push(s[i - 1] + (dx * dx + dy * dy).sqrt());
555 }
556
557 let sx = CubicSpline::new(&s, x);
558 let sy = CubicSpline::new(&s, y);
559
560 CubicSpline2D { s, sx, sy }
561 }
562
563 fn calc_position(&self, s: f64) -> (f64, f64) {
564 let x = self.sx.calc(s);
565 let y = self.sy.calc(s);
566 (x, y)
567 }
568
569 fn calc_curvature(&self, s: f64) -> f64 {
570 let dx = self.sx.calc_d(s);
571 let ddx = self.sx.calc_dd(s);
572 let dy = self.sy.calc_d(s);
573 let ddy = self.sy.calc_dd(s);
574 let denom = (dx * dx + dy * dy).powf(1.5);
575 if denom.abs() < 1e-10 {
576 0.0
577 } else {
578 (ddy * dx - ddx * dy) / denom
579 }
580 }
581
582 fn calc_yaw(&self, s: f64) -> f64 {
583 let dx = self.sx.calc_d(s);
584 let dy = self.sy.calc_d(s);
585 dy.atan2(dx)
586 }
587}
588
589#[cfg(test)]
590mod tests {
591 use super::*;
592
593 #[test]
594 fn test_rear_wheel_feedback_creation() {
595 let config = RearWheelFeedbackConfig::default();
596 let controller = RearWheelFeedbackController::new(config);
597 assert!(controller.path.is_empty());
598 }
599
600 #[test]
601 fn test_rear_wheel_feedback_config_defaults() {
602 let config = RearWheelFeedbackConfig::default();
603 assert!((config.kth - 1.0).abs() < 1e-10);
604 assert!((config.ke - 0.5).abs() < 1e-10);
605 assert!((config.wheelbase - 2.9).abs() < 1e-10);
606 }
607
608 #[test]
609 fn test_rear_wheel_feedback_set_path() {
610 let mut controller = RearWheelFeedbackController::with_params(1.0, 0.5, 2.9);
611 let path = Path2D::from_points(vec![
612 Point2D::new(0.0, 0.0),
613 Point2D::new(1.0, 0.0),
614 Point2D::new(2.0, 0.0),
615 ]);
616 controller.set_path(path);
617 assert_eq!(controller.get_path().len(), 3);
618 assert_eq!(controller.path_yaw.len(), 3);
619 assert_eq!(controller.path_curvature.len(), 3);
620 }
621
622 #[test]
623 fn test_rear_wheel_feedback_normalize_angle() {
624 assert!((RearWheelFeedbackController::normalize_angle(3.0 * PI) - PI).abs() < 0.01);
625 assert!((RearWheelFeedbackController::normalize_angle(-3.0 * PI) + PI).abs() < 0.01);
626 assert!((RearWheelFeedbackController::normalize_angle(0.5)).abs() - 0.5 < 0.01);
627 }
628
629 #[test]
630 fn test_rear_wheel_feedback_steering_straight_path() {
631 let mut controller = RearWheelFeedbackController::with_params(1.0, 0.5, 2.9);
632 let path = Path2D::from_points(vec![
633 Point2D::new(0.0, 0.0),
634 Point2D::new(10.0, 0.0),
635 Point2D::new(20.0, 0.0),
636 Point2D::new(30.0, 0.0),
637 ]);
638 controller.set_path(path);
639
640 let state = VehicleState::new(5.0, 0.0, 0.0, 5.0, 2.9);
642 let steering = controller.compute_steering(&state);
643
644 assert!(steering.abs() < 0.1);
646 }
647
648 #[test]
649 fn test_rear_wheel_feedback_steering_lateral_error() {
650 let mut controller = RearWheelFeedbackController::with_params(1.0, 0.5, 2.9);
651 let path = Path2D::from_points(vec![
652 Point2D::new(0.0, 0.0),
653 Point2D::new(10.0, 0.0),
654 Point2D::new(20.0, 0.0),
655 Point2D::new(30.0, 0.0),
656 ]);
657 controller.set_path(path);
658
659 let state = VehicleState::new(5.0, 2.0, 0.1, 5.0, 2.9);
660 let steering = controller.compute_steering(&state);
661
662 assert!(
663 steering < 0.0,
664 "Steering should be negative to correct errors, got: {}",
665 steering
666 );
667 }
668
669 #[test]
670 fn test_rear_wheel_feedback_planning() {
671 let mut controller = RearWheelFeedbackController::with_params(1.0, 0.5, 2.9);
672 let waypoints = vec![(0.0, 0.0), (50.0, 0.0), (100.0, 0.0)];
673
674 let result = controller.planning(waypoints, 5.0, 0.5);
675 assert!(result.is_some());
676 assert!(!result.unwrap().is_empty());
677 }
678
679 #[test]
680 fn test_rear_wheel_feedback_curved_path() {
681 let mut controller = RearWheelFeedbackController::with_params(1.0, 0.5, 2.9);
682 let waypoints = vec![(0.0, 0.0), (10.0, 0.0), (20.0, 5.0), (30.0, 10.0)];
683
684 let result = controller.planning(waypoints, 3.0, 0.5);
685 assert!(result.is_some());
686
687 let trajectory = result.unwrap();
688 assert!(!trajectory.is_empty());
689
690 let last = trajectory.last().unwrap();
692 let dx = last.0 - 30.0;
693 let dy = last.1 - 10.0;
694 let dist = (dx * dx + dy * dy).sqrt();
695 assert!(dist < 2.0, "Final distance to goal: {}", dist);
696 }
697
698 #[test]
699 fn test_vehicle_state_update() {
700 let mut state = VehicleState::new(0.0, 0.0, 0.0, 1.0, 2.9);
701 state.update(0.0, 0.0, 1.0);
702
703 assert!((state.x - 1.0).abs() < 0.01);
705 assert!(state.y.abs() < 0.01);
706 assert!(state.yaw.abs() < 0.01);
707 }
708
709 #[test]
710 fn test_path_tracker_trait() {
711 let mut controller = RearWheelFeedbackController::new(RearWheelFeedbackConfig::default());
712 let path = Path2D::from_points(vec![
713 Point2D::new(0.0, 0.0),
714 Point2D::new(10.0, 0.0),
715 Point2D::new(20.0, 0.0),
716 ]);
717
718 let state = State2D::new(5.0, 0.0, 0.0, 5.0);
719 let control = controller.compute_control(&state, &path);
720
721 assert!(control.v.is_finite());
723 assert!(control.omega.is_finite());
724 }
725}