1use nalgebra::{Matrix2, Matrix5, Matrix5x2, SMatrix, SVector, Vector2, Vector5};
15use rust_robotics_core::{ControlInput, Path2D, PathTracker, Point2D, State2D};
16use std::f64::consts::PI;
17
18#[derive(Debug, Clone, Copy)]
20pub struct LQRSpeedSteerVehicleState {
21 pub x: f64,
22 pub y: f64,
23 pub yaw: f64,
24 pub v: f64,
25 pub wheelbase: f64,
26 pub max_steer: f64,
27}
28
29impl LQRSpeedSteerVehicleState {
30 pub fn new(x: f64, y: f64, yaw: f64, v: f64, wheelbase: f64, max_steer: f64) -> Self {
31 Self {
32 x,
33 y,
34 yaw,
35 v,
36 wheelbase,
37 max_steer,
38 }
39 }
40
41 pub fn update(&mut self, a: f64, mut delta: f64, dt: f64) {
43 delta = delta.clamp(-self.max_steer, self.max_steer);
44 self.x += self.v * self.yaw.cos() * dt;
45 self.y += self.v * self.yaw.sin() * dt;
46 self.yaw += self.v / self.wheelbase * delta.tan() * dt;
47 self.v += a * dt;
48 }
49
50 pub fn to_state2d(&self) -> State2D {
51 State2D::new(self.x, self.y, self.yaw, self.v)
52 }
53}
54
55impl From<State2D> for LQRSpeedSteerVehicleState {
56 fn from(s: State2D) -> Self {
57 Self::new(s.x, s.y, s.yaw, s.v, 0.5, 45.0_f64.to_radians())
58 }
59}
60
61#[derive(Debug, Clone)]
63pub struct LQRSpeedSteerConfig {
64 pub wheelbase: f64,
66 pub max_steer: f64,
68 pub q_diag: [f64; 5],
70 pub r_diag: [f64; 2],
72 pub dt: f64,
74 pub goal_threshold: f64,
76}
77
78impl Default for LQRSpeedSteerConfig {
79 fn default() -> Self {
80 Self {
81 wheelbase: 0.5,
82 max_steer: 45.0_f64.to_radians(),
83 q_diag: [1.0, 1.0, 1.0, 1.0, 1.0],
84 r_diag: [1.0, 1.0],
85 dt: 0.1,
86 goal_threshold: 0.3,
87 }
88 }
89}
90
91pub struct LQRSpeedSteerController {
97 config: LQRSpeedSteerConfig,
98 path: Path2D,
99 path_yaw: Vec<f64>,
100 path_curvature: Vec<f64>,
101 speed_profile: Vec<f64>,
102 prev_error: f64,
103 prev_theta_error: f64,
104}
105
106impl LQRSpeedSteerController {
107 pub fn new(config: LQRSpeedSteerConfig) -> Self {
109 Self {
110 config,
111 path: Path2D::new(),
112 path_yaw: Vec::new(),
113 path_curvature: Vec::new(),
114 speed_profile: Vec::new(),
115 prev_error: 0.0,
116 prev_theta_error: 0.0,
117 }
118 }
119
120 pub fn with_defaults() -> Self {
122 Self::new(LQRSpeedSteerConfig::default())
123 }
124
125 pub fn set_path(&mut self, path: Path2D) {
127 let (yaw, curvature) = self.compute_path_derivatives(&path);
128 self.path_yaw = yaw;
129 self.path_curvature = curvature;
130 self.speed_profile = self.calc_speed_profile(&path, 10.0 / 3.6);
131 self.path = path;
132 self.prev_error = 0.0;
133 self.prev_theta_error = 0.0;
134 }
135
136 pub fn set_path_with_speed(&mut self, path: Path2D, target_speed: f64) {
138 let (yaw, curvature) = self.compute_path_derivatives(&path);
139 self.path_yaw = yaw;
140 self.path_curvature = curvature;
141 self.speed_profile = self.calc_speed_profile(&path, target_speed);
142 self.path = path;
143 self.prev_error = 0.0;
144 self.prev_theta_error = 0.0;
145 }
146
147 pub fn get_path(&self) -> &Path2D {
149 &self.path
150 }
151
152 fn compute_path_derivatives(&self, path: &Path2D) -> (Vec<f64>, Vec<f64>) {
154 let n = path.len();
155 let yaw = path.yaw_profile();
156 let mut curvature = Vec::with_capacity(n);
157
158 for i in 0..n {
159 if i > 0 && i < n - 1 {
160 let dyaw = Self::normalize_angle(yaw[i] - yaw[i - 1]);
161 let dx = path.points[i].x - path.points[i - 1].x;
162 let dy = path.points[i].y - path.points[i - 1].y;
163 let ds = (dx * dx + dy * dy).sqrt();
164 curvature.push(if ds > 0.001 { dyaw / ds } else { 0.0 });
165 } else {
166 curvature.push(0.0);
167 }
168 }
169
170 (yaw, curvature)
171 }
172
173 fn calc_speed_profile(&self, path: &Path2D, target_speed: f64) -> Vec<f64> {
175 let n = path.len();
176 let mut profile = vec![target_speed; n];
177 let mut direction = 1.0;
178
179 #[allow(clippy::needless_range_loop)]
180 for i in 0..n.saturating_sub(1) {
181 if i < self.path_yaw.len() - 1 {
182 let dyaw = (self.path_yaw[i + 1] - self.path_yaw[i]).abs();
183 let switch = (PI / 4.0..PI / 2.0).contains(&dyaw);
184
185 if switch {
186 direction *= -1.0;
187 }
188
189 if direction != 1.0 {
190 profile[i] = -target_speed;
191 } else {
192 profile[i] = target_speed;
193 }
194
195 if switch {
196 profile[i] = 0.0;
197 }
198 }
199 }
200
201 let slowdown_len = 40.min(n);
203 for i in 0..slowdown_len {
204 let idx = n - 1 - i;
205 profile[idx] = target_speed / (50 - i) as f64;
206 if profile[idx] <= 1.0 / 3.6 {
207 profile[idx] = 1.0 / 3.6;
208 }
209 }
210
211 profile
212 }
213
214 fn normalize_angle(mut angle: f64) -> f64 {
216 while angle > PI {
217 angle -= 2.0 * PI;
218 }
219 while angle < -PI {
220 angle += 2.0 * PI;
221 }
222 angle
223 }
224
225 fn calc_nearest_index(&self, state: &LQRSpeedSteerVehicleState) -> (usize, f64) {
227 let query = Point2D::new(state.x, state.y);
228 let min_idx = self.path.nearest_point_index(query).unwrap_or(0);
229 let target = &self.path.points[min_idx];
230 let min_dist = ((state.x - target.x).powi(2) + (state.y - target.y).powi(2)).sqrt();
231
232 let diff_x = target.x - state.x;
233 let diff_y = target.y - state.y;
234 let arcang = self.path_yaw[min_idx] - diff_y.atan2(diff_x);
235 let angle = Self::normalize_angle(arcang);
236
237 let error = if angle < 0.0 { -min_dist } else { min_dist };
238 (min_idx, error)
239 }
240
241 fn solve_dare(
243 a: &Matrix5<f64>,
244 b: &Matrix5x2<f64>,
245 q: &Matrix5<f64>,
246 r: &Matrix2<f64>,
247 ) -> Matrix5<f64> {
248 let mut x = *q;
249 let max_iter = 150;
250 let eps = 0.01;
251
252 for _ in 0..max_iter {
253 let at = a.transpose();
255 let bt = b.transpose();
256 let at_x = at * x;
257 let at_x_a = at_x * a;
258 let at_x_b = at_x * b;
259 let bt_x_b: Matrix2<f64> = bt * x * b;
260 let inv = (r + bt_x_b).try_inverse().unwrap_or(Matrix2::identity());
261 let bt_x_a: SMatrix<f64, 2, 5> = bt * x * a;
262 let x_next = at_x_a - at_x_b * inv * bt_x_a + q;
263
264 let diff: f64 = (x_next - x).abs().max();
265 if diff < eps {
266 return x_next;
267 }
268 x = x_next;
269 }
270 x
271 }
272
273 fn dlqr(
275 a: &Matrix5<f64>,
276 b: &Matrix5x2<f64>,
277 q: &Matrix5<f64>,
278 r: &Matrix2<f64>,
279 ) -> SMatrix<f64, 2, 5> {
280 let x = Self::solve_dare(a, b, q, r);
281 let bt = b.transpose();
282 let bt_x_b: Matrix2<f64> = bt * x * b;
283 let inv = (bt_x_b + r).try_inverse().unwrap_or(Matrix2::identity());
284 inv * (bt * x * a)
285 }
286
287 pub fn compute_control_full(
291 &mut self,
292 state: &LQRSpeedSteerVehicleState,
293 ) -> (f64, usize, f64, f64, f64) {
294 if self.path.is_empty() {
295 return (0.0, 0, 0.0, 0.0, 0.0);
296 }
297
298 let (ind, e) = self.calc_nearest_index(state);
299 let tv = if ind < self.speed_profile.len() {
300 self.speed_profile[ind]
301 } else {
302 0.0
303 };
304 let k = if ind < self.path_curvature.len() {
305 self.path_curvature[ind]
306 } else {
307 0.0
308 };
309
310 let v = state.v;
311 let th_e = Self::normalize_angle(state.yaw - self.path_yaw[ind]);
312 let dt = self.config.dt;
313 let l = self.config.wheelbase;
314
315 let mut a = Matrix5::zeros();
317 a[(0, 0)] = 1.0;
318 a[(0, 1)] = dt;
319 a[(1, 2)] = v;
320 a[(2, 2)] = 1.0;
321 a[(2, 3)] = dt;
322 a[(4, 4)] = 1.0;
323
324 let mut b = Matrix5x2::zeros();
326 b[(3, 0)] = v / l;
327 b[(4, 1)] = dt;
328
329 let q = Matrix5::from_diagonal(&Vector5::new(
331 self.config.q_diag[0],
332 self.config.q_diag[1],
333 self.config.q_diag[2],
334 self.config.q_diag[3],
335 self.config.q_diag[4],
336 ));
337 let r = Matrix2::from_diagonal(&Vector2::new(self.config.r_diag[0], self.config.r_diag[1]));
338
339 let gain = Self::dlqr(&a, &b, &q, &r);
340
341 let x_err = Vector5::new(
343 e,
344 if dt > 0.0 {
345 (e - self.prev_error) / dt
346 } else {
347 0.0
348 },
349 th_e,
350 if dt > 0.0 {
351 (th_e - self.prev_theta_error) / dt
352 } else {
353 0.0
354 },
355 v - tv,
356 );
357
358 let ustar: SVector<f64, 2> = -gain * x_err;
360
361 let ff = (l * k).atan2(1.0);
363 let fb = Self::normalize_angle(ustar[0]);
364 let delta = ff + fb;
365
366 let accel = ustar[1];
368
369 self.prev_error = e;
370 self.prev_theta_error = th_e;
371
372 let delta_clamped = delta.clamp(-state.max_steer, state.max_steer);
373 (delta_clamped, ind, e, th_e, accel)
374 }
375
376 pub fn compute_steering(&mut self, state: &LQRSpeedSteerVehicleState) -> f64 {
378 self.compute_control_full(state).0
379 }
380
381 pub fn compute_acceleration(&mut self, state: &LQRSpeedSteerVehicleState) -> f64 {
383 self.compute_control_full(state).4
384 }
385
386 pub fn get_target_speed(&self, index: usize) -> f64 {
388 if index < self.speed_profile.len() {
389 self.speed_profile[index]
390 } else {
391 0.0
392 }
393 }
394
395 pub fn is_goal_reached(&self, state: &LQRSpeedSteerVehicleState) -> bool {
397 if let Some(goal) = self.path.points.last() {
398 let dx = state.x - goal.x;
399 let dy = state.y - goal.y;
400 (dx * dx + dy * dy).sqrt() < self.config.goal_threshold
401 } else {
402 true
403 }
404 }
405
406 pub fn planning(
408 &mut self,
409 waypoints: Vec<(f64, f64)>,
410 target_speed: f64,
411 ds: f64,
412 ) -> Option<Vec<(f64, f64)>> {
413 if waypoints.len() < 2 {
414 return None;
415 }
416
417 let ax: Vec<f64> = waypoints.iter().map(|p| p.0).collect();
418 let ay: Vec<f64> = waypoints.iter().map(|p| p.1).collect();
419 let (cx, cy, cyaw, ck, _) = calc_spline_course(&ax, &ay, ds);
420
421 let path = Path2D::from_points(
422 cx.iter()
423 .zip(cy.iter())
424 .map(|(&x, &y)| Point2D::new(x, y))
425 .collect(),
426 );
427 self.path_yaw = cyaw;
428 self.path_curvature = ck;
429 self.speed_profile = self.calc_speed_profile(&path, target_speed);
430 self.path = path;
431 self.prev_error = 0.0;
432 self.prev_theta_error = 0.0;
433
434 let mut state = LQRSpeedSteerVehicleState::new(
435 0.0,
436 0.0,
437 0.0,
438 0.0,
439 self.config.wheelbase,
440 self.config.max_steer,
441 );
442
443 let mut trajectory = vec![(state.x, state.y)];
444 let dt = self.config.dt;
445 let t_max = 500.0;
446 let stop_speed = 0.05;
447 let mut time = 0.0;
448
449 while time < t_max {
450 let (delta, target_ind, _, _, accel) = self.compute_control_full(&state);
451
452 state.update(accel, delta, dt);
453 time += dt;
454
455 if state.v.abs() <= stop_speed && target_ind + 1 < self.path.len() {
456 }
458
459 trajectory.push((state.x, state.y));
460
461 if self.is_goal_reached(&state) {
462 break;
463 }
464 }
465
466 Some(trajectory)
467 }
468}
469
470impl PathTracker for LQRSpeedSteerController {
471 fn compute_control(&mut self, current_state: &State2D, path: &Path2D) -> ControlInput {
472 if self.path.len() != path.len() {
473 self.set_path(path.clone());
474 }
475
476 let vehicle_state = LQRSpeedSteerVehicleState::new(
477 current_state.x,
478 current_state.y,
479 current_state.yaw,
480 current_state.v,
481 self.config.wheelbase,
482 self.config.max_steer,
483 );
484
485 let (delta, _, _, _, accel) = self.compute_control_full(&vehicle_state);
486
487 let v = current_state.v + accel * self.config.dt;
488 let omega = v * delta.tan() / self.config.wheelbase;
489
490 ControlInput::new(v, omega)
491 }
492
493 fn is_goal_reached(&self, current_state: &State2D, goal: Point2D) -> bool {
494 let dx = current_state.x - goal.x;
495 let dy = current_state.y - goal.y;
496 (dx * dx + dy * dy).sqrt() < self.config.goal_threshold
497 }
498}
499
500fn calc_spline_course(x: &[f64], y: &[f64], ds: f64) -> SplineCourse {
503 let sp = CubicSpline2D::new(x, y);
504 let mut s = 0.0;
505 let mut course_x = Vec::new();
506 let mut course_y = Vec::new();
507 let mut course_yaw = Vec::new();
508 let mut course_k = Vec::new();
509 let mut course_s = Vec::new();
510
511 let s_max = *sp
513 .s
514 .last()
515 .expect("spline s is non-empty after construction")
516 - ds;
517 while s < s_max {
518 let (ix, iy) = sp.calc_position(s);
519 let iyaw = sp.calc_yaw(s);
520 let ik = sp.calc_curvature(s);
521 course_x.push(ix);
522 course_y.push(iy);
523 course_yaw.push(iyaw);
524 course_k.push(ik);
525 course_s.push(s);
526 s += ds;
527 }
528
529 (course_x, course_y, course_yaw, course_k, course_s)
530}
531
532struct CubicSpline {
533 a: Vec<f64>,
534 b: Vec<f64>,
535 c: Vec<f64>,
536 d: Vec<f64>,
537 x: Vec<f64>,
538}
539
540type SplineCourse = (Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>);
541
542impl CubicSpline {
543 fn new(x: &[f64], y: &[f64]) -> Self {
544 let n = x.len();
545 let mut h = vec![0.0; n - 1];
546 for i in 0..n - 1 {
547 h[i] = x[i + 1] - x[i];
548 }
549
550 let mut a = vec![0.0; n];
551 let mut b = vec![0.0; n];
552 let mut c = vec![0.0; n];
553 let mut d = vec![0.0; n];
554
555 a[..n].copy_from_slice(&y[..n]);
556
557 let mut alpha = vec![0.0; n - 1];
558 for i in 1..n - 1 {
559 alpha[i] = 3.0 * (a[i + 1] - a[i]) / h[i] - 3.0 * (a[i] - a[i - 1]) / h[i - 1];
560 }
561
562 let mut l = vec![1.0; n];
563 let mut mu = vec![0.0; n];
564 let mut z = vec![0.0; n];
565
566 for i in 1..n - 1 {
567 l[i] = 2.0 * (x[i + 1] - x[i - 1]) - h[i - 1] * mu[i - 1];
568 mu[i] = h[i] / l[i];
569 z[i] = (alpha[i] - h[i - 1] * z[i - 1]) / l[i];
570 }
571
572 for j in (0..n - 1).rev() {
573 c[j] = z[j] - mu[j] * c[j + 1];
574 b[j] = (a[j + 1] - a[j]) / h[j] - h[j] * (c[j + 1] + 2.0 * c[j]) / 3.0;
575 d[j] = (c[j + 1] - c[j]) / (3.0 * h[j]);
576 }
577
578 CubicSpline {
579 a,
580 b,
581 c,
582 d,
583 x: x.to_vec(),
584 }
585 }
586
587 fn calc(&self, t: f64) -> f64 {
588 if t < self.x[0] {
589 return self.a[0];
590 } else if t > self.x[self.x.len() - 1] {
591 return self.a[self.a.len() - 1];
592 }
593
594 let mut i = self.search_index(t);
595 if i >= self.x.len() - 1 {
596 i = self.x.len() - 2;
597 }
598
599 let dx = t - self.x[i];
600 self.a[i] + self.b[i] * dx + self.c[i] * dx * dx + self.d[i] * dx * dx * dx
601 }
602
603 fn calc_d(&self, t: f64) -> f64 {
604 if t < self.x[0] {
605 return self.b[0];
606 } else if t > self.x[self.x.len() - 1] {
607 return self.b[self.b.len() - 1];
608 }
609
610 let mut i = self.search_index(t);
611 if i >= self.x.len() - 1 {
612 i = self.x.len() - 2;
613 }
614
615 let dx = t - self.x[i];
616 self.b[i] + 2.0 * self.c[i] * dx + 3.0 * self.d[i] * dx * dx
617 }
618
619 fn calc_dd(&self, t: f64) -> f64 {
620 if t < self.x[0] {
621 return 2.0 * self.c[0];
622 } else if t > self.x[self.x.len() - 1] {
623 return 2.0 * self.c[self.c.len() - 1];
624 }
625
626 let mut i = self.search_index(t);
627 if i >= self.x.len() - 1 {
628 i = self.x.len() - 2;
629 }
630
631 let dx = t - self.x[i];
632 2.0 * self.c[i] + 6.0 * self.d[i] * dx
633 }
634
635 fn search_index(&self, x: f64) -> usize {
636 for i in 0..self.x.len() - 1 {
637 if self.x[i] <= x && x <= self.x[i + 1] {
638 return i;
639 }
640 }
641 self.x.len() - 2
642 }
643}
644
645struct CubicSpline2D {
646 s: Vec<f64>,
647 sx: CubicSpline,
648 sy: CubicSpline,
649}
650
651impl CubicSpline2D {
652 fn new(x: &[f64], y: &[f64]) -> Self {
653 let mut s = vec![0.0];
654 for i in 1..x.len() {
655 let dx = x[i] - x[i - 1];
656 let dy = y[i] - y[i - 1];
657 s.push(s[i - 1] + (dx * dx + dy * dy).sqrt());
658 }
659
660 let sx = CubicSpline::new(&s, x);
661 let sy = CubicSpline::new(&s, y);
662
663 CubicSpline2D { s, sx, sy }
664 }
665
666 fn calc_position(&self, s: f64) -> (f64, f64) {
667 (self.sx.calc(s), self.sy.calc(s))
668 }
669
670 fn calc_curvature(&self, s: f64) -> f64 {
671 let dx = self.sx.calc_d(s);
672 let ddx = self.sx.calc_dd(s);
673 let dy = self.sy.calc_d(s);
674 let ddy = self.sy.calc_dd(s);
675 let denom = (dx * dx + dy * dy).powf(1.5);
676 if denom.abs() > 1e-6 {
677 (ddy * dx - ddx * dy) / denom
678 } else {
679 0.0
680 }
681 }
682
683 fn calc_yaw(&self, s: f64) -> f64 {
684 let dx = self.sx.calc_d(s);
685 let dy = self.sy.calc_d(s);
686 dy.atan2(dx)
687 }
688}
689
690#[cfg(test)]
691mod tests {
692 use super::*;
693
694 #[test]
695 fn test_creation() {
696 let config = LQRSpeedSteerConfig::default();
697 let controller = LQRSpeedSteerController::new(config);
698 assert!(controller.path.is_empty());
699 }
700
701 #[test]
702 fn test_default_config() {
703 let config = LQRSpeedSteerConfig::default();
704 assert_eq!(config.q_diag.len(), 5);
705 assert_eq!(config.r_diag.len(), 2);
706 assert!((config.wheelbase - 0.5).abs() < 1e-9);
707 assert!((config.dt - 0.1).abs() < 1e-9);
708 }
709
710 #[test]
711 fn test_set_path() {
712 let mut controller = LQRSpeedSteerController::with_defaults();
713 let path = Path2D::from_points(vec![
714 Point2D::new(0.0, 0.0),
715 Point2D::new(1.0, 0.0),
716 Point2D::new(2.0, 0.0),
717 Point2D::new(3.0, 0.0),
718 ]);
719 controller.set_path(path);
720 assert_eq!(controller.get_path().len(), 4);
721 assert_eq!(controller.path_yaw.len(), 4);
722 assert_eq!(controller.path_curvature.len(), 4);
723 assert_eq!(controller.speed_profile.len(), 4);
724 }
725
726 #[test]
727 fn test_set_path_with_speed() {
728 let mut controller = LQRSpeedSteerController::with_defaults();
729 let path = Path2D::from_points(vec![
730 Point2D::new(0.0, 0.0),
731 Point2D::new(1.0, 0.0),
732 Point2D::new(2.0, 0.0),
733 ]);
734 controller.set_path_with_speed(path, 5.0);
735 assert_eq!(controller.get_path().len(), 3);
736 }
737
738 #[test]
739 fn test_normalize_angle() {
740 let a = LQRSpeedSteerController::normalize_angle(3.0 * PI);
741 assert!((a - PI).abs() < 0.01);
742 let b = LQRSpeedSteerController::normalize_angle(-3.0 * PI);
743 assert!((b + PI).abs() < 0.01);
744 let c = LQRSpeedSteerController::normalize_angle(0.5);
745 assert!((c - 0.5).abs() < 1e-9);
746 }
747
748 #[test]
749 fn test_solve_dare_identity() {
750 let a = Matrix5::identity();
751 let mut b = Matrix5x2::zeros();
752 b[(3, 0)] = 1.0;
753 b[(4, 1)] = 0.1;
754 let q = Matrix5::identity();
755 let r = Matrix2::identity();
756
757 let x = LQRSpeedSteerController::solve_dare(&a, &b, &q, &r);
758 for i in 0..5 {
760 assert!(x[(i, i)] >= 0.0);
761 }
762 }
763
764 #[test]
765 fn test_dlqr_gain() {
766 let a = Matrix5::identity();
767 let mut b = Matrix5x2::zeros();
768 b[(3, 0)] = 1.0;
769 b[(4, 1)] = 0.1;
770 let q = Matrix5::identity();
771 let r = Matrix2::identity();
772
773 let k = LQRSpeedSteerController::dlqr(&a, &b, &q, &r);
774 assert_eq!(k.nrows(), 2);
776 assert_eq!(k.ncols(), 5);
777 }
778
779 #[test]
780 fn test_compute_control_empty_path() {
781 let mut controller = LQRSpeedSteerController::with_defaults();
782 let state = LQRSpeedSteerVehicleState::new(0.0, 0.0, 0.0, 1.0, 0.5, 0.78);
783 let (delta, ind, e, th_e, accel) = controller.compute_control_full(&state);
784 assert_eq!(delta, 0.0);
785 assert_eq!(ind, 0);
786 assert_eq!(e, 0.0);
787 assert_eq!(th_e, 0.0);
788 assert_eq!(accel, 0.0);
789 }
790
791 #[test]
792 fn test_compute_control_straight_path() {
793 let mut controller = LQRSpeedSteerController::with_defaults();
794 let path =
795 Path2D::from_points((0..50).map(|i| Point2D::new(i as f64 * 0.5, 0.0)).collect());
796 controller.set_path_with_speed(path, 2.0);
797
798 let state = LQRSpeedSteerVehicleState::new(0.0, 0.0, 0.0, 1.0, 0.5, 0.78);
799 let (delta, _ind, _e, _th_e, _accel) = controller.compute_control_full(&state);
800 assert!(delta.abs() < 0.5);
802 }
803
804 #[test]
805 fn test_vehicle_state_update() {
806 let mut state = LQRSpeedSteerVehicleState::new(0.0, 0.0, 0.0, 1.0, 0.5, 0.78);
807 state.update(0.0, 0.0, 0.1);
808 assert!((state.x - 0.1).abs() < 1e-9);
809 assert!(state.y.abs() < 1e-9);
810 }
811
812 #[test]
813 fn test_vehicle_state_update_with_steer() {
814 let mut state = LQRSpeedSteerVehicleState::new(0.0, 0.0, 0.0, 1.0, 0.5, 0.78);
815 state.update(0.0, 0.1, 0.1);
816 assert!(state.x > 0.0);
818 assert!(state.yaw.abs() > 0.0);
819 }
820
821 #[test]
822 fn test_vehicle_state_steer_clamping() {
823 let mut state = LQRSpeedSteerVehicleState::new(0.0, 0.0, 0.0, 1.0, 0.5, 0.1);
824 state.update(0.0, 1.0, 0.1); let expected_yaw = 1.0 / 0.5 * (0.1_f64).tan() * 0.1;
827 assert!((state.yaw - expected_yaw).abs() < 1e-9);
828 }
829
830 #[test]
831 fn test_from_state2d() {
832 let s = State2D::new(1.0, 2.0, 0.5, 3.0);
833 let vs = LQRSpeedSteerVehicleState::from(s);
834 assert!((vs.x - 1.0).abs() < 1e-9);
835 assert!((vs.y - 2.0).abs() < 1e-9);
836 assert!((vs.yaw - 0.5).abs() < 1e-9);
837 assert!((vs.v - 3.0).abs() < 1e-9);
838 }
839
840 #[test]
841 fn test_to_state2d() {
842 let vs = LQRSpeedSteerVehicleState::new(1.0, 2.0, 0.5, 3.0, 0.5, 0.78);
843 let s = vs.to_state2d();
844 assert!((s.x - 1.0).abs() < 1e-9);
845 assert!((s.y - 2.0).abs() < 1e-9);
846 }
847
848 #[test]
849 fn test_is_goal_reached() {
850 let mut controller = LQRSpeedSteerController::with_defaults();
851 let path = Path2D::from_points(vec![Point2D::new(0.0, 0.0), Point2D::new(5.0, 0.0)]);
852 controller.set_path(path);
853
854 let far = LQRSpeedSteerVehicleState::new(0.0, 0.0, 0.0, 1.0, 0.5, 0.78);
855 assert!(!controller.is_goal_reached(&far));
856
857 let near = LQRSpeedSteerVehicleState::new(5.0, 0.1, 0.0, 0.0, 0.5, 0.78);
858 assert!(controller.is_goal_reached(&near));
859 }
860
861 #[test]
862 fn test_planning_straight_line() {
863 let mut controller = LQRSpeedSteerController::with_defaults();
864 let waypoints = vec![(0.0, 0.0), (5.0, 0.0), (10.0, 0.0)];
865 let result = controller.planning(waypoints, 2.0, 0.5);
866 assert!(result.is_some());
867 let traj = result.unwrap();
868 assert!(!traj.is_empty());
869 let (fx, fy) = traj.last().unwrap();
871 let dist = ((fx - 10.0).powi(2) + fy.powi(2)).sqrt();
872 assert!(dist < 2.0, "Final distance to goal: {}", dist);
873 }
874
875 #[test]
876 fn test_planning_with_curve() {
877 let mut controller = LQRSpeedSteerController::with_defaults();
878 let waypoints = vec![
879 (0.0, 0.0),
880 (6.0, -3.0),
881 (12.5, -5.0),
882 (10.0, 6.5),
883 (17.5, 3.0),
884 (20.0, 0.0),
885 (25.0, 0.0),
886 ];
887 let result = controller.planning(waypoints, 10.0 / 3.6, 0.1);
888 assert!(result.is_some());
889 let traj = result.unwrap();
890 assert!(traj.len() > 10);
891 }
892
893 #[test]
894 fn test_planning_too_few_waypoints() {
895 let mut controller = LQRSpeedSteerController::with_defaults();
896 let result = controller.planning(vec![(0.0, 0.0)], 2.0, 0.5);
897 assert!(result.is_none());
898 }
899
900 #[test]
901 fn test_path_tracker_trait() {
902 let mut controller = LQRSpeedSteerController::with_defaults();
903 let path =
904 Path2D::from_points((0..50).map(|i| Point2D::new(i as f64 * 0.5, 0.0)).collect());
905 let state = State2D::new(0.0, 0.0, 0.0, 1.0);
906 let input = controller.compute_control(&state, &path);
907 assert!(input.v.is_finite());
909 assert!(input.omega.is_finite());
910 }
911
912 #[test]
913 fn test_speed_profile_slowdown() {
914 let mut controller = LQRSpeedSteerController::with_defaults();
915 let path = Path2D::from_points(
916 (0..100)
917 .map(|i| Point2D::new(i as f64 * 0.1, 0.0))
918 .collect(),
919 );
920 controller.set_path_with_speed(path, 3.0);
921 let n = controller.speed_profile.len();
923 assert!(controller.speed_profile[n - 1] < 3.0);
924 assert!(controller.speed_profile[n - 1] > 0.0);
925 }
926
927 #[test]
928 fn test_get_target_speed() {
929 let mut controller = LQRSpeedSteerController::with_defaults();
930 let path = Path2D::from_points(vec![Point2D::new(0.0, 0.0), Point2D::new(1.0, 0.0)]);
931 controller.set_path_with_speed(path, 2.0);
932 let s = controller.get_target_speed(0);
934 assert!(s.is_finite());
935 assert_eq!(controller.get_target_speed(1000), 0.0);
937 }
938}