Skip to main content

rust_robotics_control/
lqr_speed_steer_control.rs

1//! LQR Speed and Steering Control path tracking algorithm
2//!
3//! Extends LQR steer control with speed control, tracking both lateral and
4//! longitudinal errors. The state vector is 5-dimensional:
5//!   x = \[e, dot_e, th_e, dot_th_e, delta_v\]
6//! and the input vector is 2-dimensional:
7//!   u = \[delta, accel\]
8//!
9//! Reference: PythonRobotics lqr_speed_steer_control
10//!
11//! author: Atsushi Sakai (@Atsushi_twi)
12//!         Ryohei Sasaki (@rsasaki0109)
13
14use nalgebra::{Matrix2, Matrix5, Matrix5x2, SMatrix, SVector, Vector2, Vector5};
15use rust_robotics_core::{ControlInput, Path2D, PathTracker, Point2D, State2D};
16use std::f64::consts::PI;
17
18/// Vehicle state for LQR speed+steer controller
19#[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    /// Update vehicle state with bicycle model kinematics
42    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/// Configuration for LQR Speed and Steer Controller
62#[derive(Debug, Clone)]
63pub struct LQRSpeedSteerConfig {
64    /// Vehicle wheelbase \[m\]
65    pub wheelbase: f64,
66    /// Maximum steering angle \[rad\]
67    pub max_steer: f64,
68    /// State cost matrix Q diagonal (5 elements: e, dot_e, th_e, dot_th_e, delta_v)
69    pub q_diag: [f64; 5],
70    /// Control cost matrix R diagonal (2 elements: delta, accel)
71    pub r_diag: [f64; 2],
72    /// Time step \[s\]
73    pub dt: f64,
74    /// Goal distance threshold \[m\]
75    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
91/// LQR Speed and Steer path tracking controller
92///
93/// Unlike the steer-only LQR controller, this controller computes both
94/// steering angle and acceleration via a single 5-state LQR formulation,
95/// removing the need for a separate PID speed controller.
96pub 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    /// Create a new LQR Speed+Steer controller
108    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    /// Create with default configuration
121    pub fn with_defaults() -> Self {
122        Self::new(LQRSpeedSteerConfig::default())
123    }
124
125    /// Set the reference path with a default target speed (~10 km/h)
126    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    /// Set the reference path with a specific target speed
137    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    /// Get the current reference path
148    pub fn get_path(&self) -> &Path2D {
149        &self.path
150    }
151
152    /// Compute yaw and curvature from path points
153    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    /// Calculate speed profile based on path yaw changes
174    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        // Speed down near the end
202        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    /// Normalize angle to \[-PI, PI\]
215    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    /// Find nearest path index and signed cross-track error
226    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    /// Solve Discrete Algebraic Riccati Equation for 5x5 system with 2 inputs
242    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            // x_next = A^T X A - A^T X B (R + B^T X B)^{-1} B^T X A + Q
254            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    /// Compute discrete LQR gain matrix K (2x5)
274    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    /// Compute LQR speed and steering control
288    ///
289    /// Returns (steering_delta, target_index, lateral_error, heading_error, acceleration)
290    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        // Build 5x5 state-space A matrix
316        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        // Build 5x2 input B matrix
325        let mut b = Matrix5x2::zeros();
326        b[(3, 0)] = v / l;
327        b[(4, 1)] = dt;
328
329        // Build Q and R matrices
330        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        // State error vector: [e, dot_e, th_e, dot_th_e, delta_v]
342        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        // u* = -K x
359        let ustar: SVector<f64, 2> = -gain * x_err;
360
361        // Steering: feedforward + feedback
362        let ff = (l * k).atan2(1.0);
363        let fb = Self::normalize_angle(ustar[0]);
364        let delta = ff + fb;
365
366        // Acceleration from LQR
367        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    /// Compute steering angle only (convenience method)
377    pub fn compute_steering(&mut self, state: &LQRSpeedSteerVehicleState) -> f64 {
378        self.compute_control_full(state).0
379    }
380
381    /// Compute acceleration only (convenience method)
382    pub fn compute_acceleration(&mut self, state: &LQRSpeedSteerVehicleState) -> f64 {
383        self.compute_control_full(state).4
384    }
385
386    /// Get target speed at a given index
387    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    /// Check if goal is reached
396    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    /// Run a full simulation using cubic spline interpolation of waypoints
407    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                // nudge forward when nearly stopped
457            }
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
500// --- Cubic spline helpers (same as in lqr_steer_control) ---
501
502fn 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    // sp.s is always non-empty: CubicSpline2D::new initializes s with at least one element
512    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        // Result should be positive semi-definite (diagonal >= 0)
759        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        // K should be 2x5
775        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        // On a straight path aligned with the vehicle, steering should be near zero
801        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        // Should have moved forward and slightly turned
817        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); // delta=1.0 exceeds max_steer=0.1
825                                     // yaw change should be limited by max_steer
826        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        // Final position should be near the goal
870        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        // Should produce some valid control input
908        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        // Speed near the end should be less than target speed
922        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        // Valid index
933        let s = controller.get_target_speed(0);
934        assert!(s.is_finite());
935        // Out of bounds should return 0
936        assert_eq!(controller.get_target_speed(1000), 0.0);
937    }
938}