Skip to main content

rust_robotics_control/
mpc.rs

1#![allow(dead_code)]
2
3//! Model Predictive Control (MPC) for path tracking
4//!
5//! author: Atsushi Sakai (@Atsushi_twi)
6//!         Ryohei Sasaki (@rsasaki0109)
7//!         Rust port
8//!
9//! This version follows the PythonRobotics structure more closely:
10//! - speed profile generation
11//! - yaw smoothing
12//! - iterative linear MPC around an operational point
13
14use nalgebra::{Matrix2, Matrix4, Matrix4x2, Vector2, Vector4};
15use std::f64::consts::PI;
16
17// Vehicle parameters
18const WB: f64 = 2.5; // wheelbase [m]
19const MAX_STEER: f64 = 45.0 * PI / 180.0; // max steering angle [rad]
20const MAX_DSTEER: f64 = 30.0 * PI / 180.0; // max steering rate [rad/s]
21const MAX_SPEED: f64 = 55.0 / 3.6; // max speed [m/s]
22const MIN_SPEED: f64 = -20.0 / 3.6; // min speed (reverse) [m/s]
23const MAX_ACCEL: f64 = 1.0; // max acceleration [m/ss]
24
25// MPC parameters, aligned with PythonRobotics
26const T: usize = 5; // prediction horizon
27const DT: f64 = 0.2; // time step [s]
28const TARGET_SPEED: f64 = 10.0 / 3.6; // [m/s]
29const GOAL_DIS: f64 = 1.5; // goal distance threshold
30const STOP_SPEED: f64 = 0.5 / 3.6; // stop speed threshold
31const MAX_ITER: usize = 3; // iterative linear MPC outer iterations
32const DU_TH: f64 = 0.1; // outer-loop convergence threshold
33const N_IND_SEARCH: usize = 10; // nearest-index search window
34const PATH_RESOLUTION: f64 = 0.5; // [m]
35const UPSTREAM_SWITCH_BACK_TICK: f64 = 1.0; // [m]
36const MAX_SIM_STEPS: usize = 2000;
37
38// Inner projected-gradient solver settings
39const QP_MAX_ITERS: usize = 200;
40const LINE_SEARCH_ITERS: usize = 16;
41const LINE_SEARCH_INITIAL_STEP: f64 = 0.25;
42const GRAD_TOL: f64 = 1.0e-5;
43const COST_TOL: f64 = 1.0e-7;
44
45// Cost weights
46const Q: [f64; 4] = [1.0, 1.0, 0.5, 0.5];
47const QF: [f64; 4] = Q;
48const R: [f64; 2] = [0.01, 0.01];
49const RD: [f64; 2] = [0.01, 1.0];
50
51/// Vehicle state
52#[derive(Clone, Copy, Debug)]
53pub struct State {
54    pub x: f64,
55    pub y: f64,
56    pub v: f64,
57    pub yaw: f64,
58}
59
60impl State {
61    pub fn new(x: f64, y: f64, v: f64, yaw: f64) -> Self {
62        Self { x, y, v, yaw }
63    }
64
65    pub fn to_vector(self) -> Vector4<f64> {
66        Vector4::new(self.x, self.y, self.v, self.yaw)
67    }
68
69    pub fn update(&mut self, accel: f64, steer: f64) {
70        let steer = steer.clamp(-MAX_STEER, MAX_STEER);
71
72        self.x += self.v * self.yaw.cos() * DT;
73        self.y += self.v * self.yaw.sin() * DT;
74        self.yaw += self.v / WB * steer.tan() * DT;
75        self.yaw = normalize_angle(self.yaw);
76        self.v += accel * DT;
77        self.v = self.v.clamp(MIN_SPEED, MAX_SPEED);
78    }
79}
80
81#[derive(Debug)]
82pub struct MpcResult {
83    pub controls: Vec<Vector2<f64>>,
84    pub predicted: Vec<Vector4<f64>>,
85}
86
87#[derive(Debug)]
88pub struct SimulationResult {
89    pub cx: Vec<f64>,
90    pub cy: Vec<f64>,
91    pub hist_x: Vec<f64>,
92    pub hist_y: Vec<f64>,
93    pub predicted_x: Vec<f64>,
94    pub predicted_y: Vec<f64>,
95    pub goal: (f64, f64),
96    pub reached_goal: bool,
97    pub target_index: usize,
98    pub final_index: usize,
99    pub final_state: State,
100}
101
102pub fn normalize_angle(angle: f64) -> f64 {
103    let mut value = angle % (2.0 * PI);
104    if value > PI {
105        value -= 2.0 * PI;
106    } else if value < -PI {
107        value += 2.0 * PI;
108    }
109    value
110}
111
112fn angle_diff(a: f64, b: f64) -> f64 {
113    normalize_angle(a - b)
114}
115
116fn state_error(state: Vector4<f64>, reference: Vector4<f64>) -> Vector4<f64> {
117    let mut error = state - reference;
118    error[3] = angle_diff(state[3], reference[3]);
119    error
120}
121
122fn control_cost_weight() -> Matrix2<f64> {
123    Matrix2::from_diagonal(&Vector2::new(R[0], R[1]))
124}
125
126fn control_rate_weight() -> Matrix2<f64> {
127    Matrix2::from_diagonal(&Vector2::new(RD[0], RD[1]))
128}
129
130fn state_cost_weight() -> Matrix4<f64> {
131    Matrix4::from_diagonal(&Vector4::new(Q[0], Q[1], Q[2], Q[3]))
132}
133
134fn terminal_cost_weight() -> Matrix4<f64> {
135    Matrix4::from_diagonal(&Vector4::new(QF[0], QF[1], QF[2], QF[3]))
136}
137
138fn get_linear_model_matrix(
139    v: f64,
140    yaw: f64,
141    steer: f64,
142) -> (Matrix4<f64>, Matrix4x2<f64>, Vector4<f64>) {
143    let a = Matrix4::new(
144        1.0,
145        0.0,
146        DT * yaw.cos(),
147        -DT * v * yaw.sin(),
148        0.0,
149        1.0,
150        DT * yaw.sin(),
151        DT * v * yaw.cos(),
152        0.0,
153        0.0,
154        1.0,
155        0.0,
156        0.0,
157        0.0,
158        DT * steer.tan() / WB,
159        1.0,
160    );
161
162    let b = Matrix4x2::new(
163        0.0,
164        0.0,
165        0.0,
166        0.0,
167        DT,
168        0.0,
169        0.0,
170        DT * v / (WB * steer.cos().powi(2)),
171    );
172
173    let c = Vector4::new(
174        DT * v * yaw.sin() * yaw,
175        -DT * v * yaw.cos() * yaw,
176        0.0,
177        -DT * v * steer / (WB * steer.cos().powi(2)),
178    );
179
180    (a, b, c)
181}
182
183/// Cubic spline for reference path
184struct CubicSpline1D {
185    x: Vec<f64>,
186    a: Vec<f64>,
187    b: Vec<f64>,
188    c: Vec<f64>,
189    d: Vec<f64>,
190}
191
192impl CubicSpline1D {
193    fn new(x: &[f64], y: &[f64]) -> Self {
194        let n = x.len();
195        let a = y.to_vec();
196        let mut b = vec![0.0; n];
197        let mut c = vec![0.0; n];
198        let mut d = vec![0.0; n];
199
200        let h: Vec<f64> = (0..n - 1).map(|i| x[i + 1] - x[i]).collect();
201
202        let mut alpha = vec![0.0; n];
203        for i in 1..n - 1 {
204            alpha[i] = 3.0 / h[i] * (a[i + 1] - a[i]) - 3.0 / h[i - 1] * (a[i] - a[i - 1]);
205        }
206
207        let mut l = vec![1.0; n];
208        let mut mu = vec![0.0; n];
209        let mut z = vec![0.0; n];
210
211        for i in 1..n - 1 {
212            l[i] = 2.0 * (x[i + 1] - x[i - 1]) - h[i - 1] * mu[i - 1];
213            mu[i] = h[i] / l[i];
214            z[i] = (alpha[i] - h[i - 1] * z[i - 1]) / l[i];
215        }
216
217        for j in (0..n - 1).rev() {
218            c[j] = z[j] - mu[j] * c[j + 1];
219            b[j] = (a[j + 1] - a[j]) / h[j] - h[j] * (c[j + 1] + 2.0 * c[j]) / 3.0;
220            d[j] = (c[j + 1] - c[j]) / (3.0 * h[j]);
221        }
222
223        Self {
224            x: x.to_vec(),
225            a,
226            b,
227            c,
228            d,
229        }
230    }
231
232    fn calc(&self, t: f64) -> f64 {
233        let i = self.search_index(t);
234        let dx = t - self.x[i];
235        self.a[i] + self.b[i] * dx + self.c[i] * dx.powi(2) + self.d[i] * dx.powi(3)
236    }
237
238    fn calc_d(&self, t: f64) -> f64 {
239        let i = self.search_index(t);
240        let dx = t - self.x[i];
241        self.b[i] + 2.0 * self.c[i] * dx + 3.0 * self.d[i] * dx.powi(2)
242    }
243
244    fn search_index(&self, t: f64) -> usize {
245        for i in 0..self.x.len() - 1 {
246            if self.x[i] <= t && t < self.x[i + 1] {
247                return i;
248            }
249        }
250        self.x.len() - 2
251    }
252}
253
254/// 2D Cubic spline path
255struct CubicSpline2D {
256    s: Vec<f64>,
257    sx: CubicSpline1D,
258    sy: CubicSpline1D,
259}
260
261impl CubicSpline2D {
262    fn new(x: &[f64], y: &[f64]) -> Self {
263        let mut s = vec![0.0];
264        for i in 1..x.len() {
265            let ds = ((x[i] - x[i - 1]).powi(2) + (y[i] - y[i - 1]).powi(2)).sqrt();
266            s.push(s[i - 1] + ds);
267        }
268
269        let sx = CubicSpline1D::new(&s, x);
270        let sy = CubicSpline1D::new(&s, y);
271
272        Self { s, sx, sy }
273    }
274
275    fn calc_position(&self, s: f64) -> (f64, f64) {
276        (self.sx.calc(s), self.sy.calc(s))
277    }
278
279    fn calc_yaw(&self, s: f64) -> f64 {
280        let dx = self.sx.calc_d(s);
281        let dy = self.sy.calc_d(s);
282        dy.atan2(dx)
283    }
284}
285
286fn smooth_yaw(yaw: &mut [f64]) {
287    for i in 0..yaw.len().saturating_sub(1) {
288        let mut dyaw = yaw[i + 1] - yaw[i];
289        while dyaw >= PI / 2.0 {
290            yaw[i + 1] -= 2.0 * PI;
291            dyaw = yaw[i + 1] - yaw[i];
292        }
293        while dyaw <= -PI / 2.0 {
294            yaw[i + 1] += 2.0 * PI;
295            dyaw = yaw[i + 1] - yaw[i];
296        }
297    }
298}
299
300pub fn calc_speed_profile(cx: &[f64], cy: &[f64], cyaw: &[f64], target_speed: f64) -> Vec<f64> {
301    let mut speed_profile = vec![target_speed; cx.len()];
302    let mut direction = 1.0;
303
304    for i in 0..cx.len().saturating_sub(1) {
305        let dx = cx[i + 1] - cx[i];
306        let dy = cy[i + 1] - cy[i];
307        let move_direction = dy.atan2(dx);
308
309        if dx.abs() > f64::EPSILON && dy.abs() > f64::EPSILON {
310            let dangle = angle_diff(move_direction, cyaw[i]).abs();
311            direction = if dangle >= PI / 4.0 { -1.0 } else { 1.0 };
312        }
313
314        speed_profile[i] = direction * target_speed;
315    }
316
317    if let Some(last) = speed_profile.last_mut() {
318        *last = 0.0;
319    }
320
321    speed_profile
322}
323
324fn calc_nearest_index(
325    state: &State,
326    cx: &[f64],
327    cy: &[f64],
328    cyaw: &[f64],
329    pind: usize,
330) -> (usize, f64) {
331    let start = pind.min(cx.len().saturating_sub(1));
332    let end = (start + N_IND_SEARCH).min(cx.len());
333
334    let mut best_index = start;
335    let mut best_distance_sq = f64::INFINITY;
336
337    for i in start..end {
338        let dx = state.x - cx[i];
339        let dy = state.y - cy[i];
340        let distance_sq = dx * dx + dy * dy;
341        if distance_sq < best_distance_sq {
342            best_distance_sq = distance_sq;
343            best_index = i;
344        }
345    }
346
347    let mut best_distance = best_distance_sq.sqrt();
348    let dxl = cx[best_index] - state.x;
349    let dyl = cy[best_index] - state.y;
350    let angle = angle_diff(cyaw[best_index], dyl.atan2(dxl));
351    if angle < 0.0 {
352        best_distance *= -1.0;
353    }
354
355    (best_index, best_distance)
356}
357
358fn calc_ref_trajectory(
359    state: &State,
360    cx: &[f64],
361    cy: &[f64],
362    cyaw: &[f64],
363    speed_profile: &[f64],
364    course_tick: f64,
365    pind: usize,
366) -> (Vec<Vector4<f64>>, usize) {
367    let ncourse = cx.len();
368    let (mut ind, _) = calc_nearest_index(state, cx, cy, cyaw, pind);
369    if pind >= ind {
370        ind = pind;
371    }
372
373    let mut xref = vec![Vector4::zeros(); T + 1];
374    xref[0] = Vector4::new(cx[ind], cy[ind], speed_profile[ind], cyaw[ind]);
375
376    let mut travel = 0.0;
377    for point in xref.iter_mut().take(T + 1).skip(1) {
378        travel += state.v.abs() * DT;
379        let dind = (travel / course_tick).round() as usize;
380        let index = (ind + dind).min(ncourse - 1);
381        *point = Vector4::new(cx[index], cy[index], speed_profile[index], cyaw[index]);
382    }
383
384    (xref, ind)
385}
386
387fn predict_motion(state: State, controls: &[Vector2<f64>]) -> Vec<Vector4<f64>> {
388    let mut predicted = vec![Vector4::zeros(); T + 1];
389    predicted[0] = state.to_vector();
390
391    let mut current = state;
392    for (i, control) in controls.iter().enumerate().take(T) {
393        current.update(control[0], control[1]);
394        predicted[i + 1] = current.to_vector();
395    }
396
397    predicted
398}
399
400fn apply_control_constraints(controls: &mut [Vector2<f64>]) {
401    for i in 0..controls.len() {
402        controls[i][0] = controls[i][0].clamp(-MAX_ACCEL, MAX_ACCEL);
403        controls[i][1] = controls[i][1].clamp(-MAX_STEER, MAX_STEER);
404
405        if i > 0 {
406            let delta = controls[i][1] - controls[i - 1][1];
407            let max_delta = MAX_DSTEER * DT;
408            if delta.abs() > max_delta {
409                controls[i][1] = controls[i - 1][1] + max_delta * delta.signum();
410            }
411        }
412    }
413}
414
415type LinearizedRollout = (
416    Vec<Vector4<f64>>,
417    Vec<Matrix4<f64>>,
418    Vec<Matrix4x2<f64>>,
419    Vec<Vector4<f64>>,
420);
421
422fn linearized_rollout(
423    x0: &State,
424    xbar: &[Vector4<f64>],
425    controls: &[Vector2<f64>],
426) -> LinearizedRollout {
427    let mut x = vec![Vector4::zeros(); T + 1];
428    let mut a_seq = Vec::with_capacity(T);
429    let mut b_seq = Vec::with_capacity(T);
430    let mut c_seq = Vec::with_capacity(T);
431
432    x[0] = x0.to_vector();
433    for t in 0..T {
434        let (a, b, c) = get_linear_model_matrix(xbar[t][2], xbar[t][3], 0.0);
435        let mut next = a * x[t] + b * controls[t] + c;
436        next[3] = normalize_angle(next[3]);
437
438        a_seq.push(a);
439        b_seq.push(b);
440        c_seq.push(c);
441        x[t + 1] = next;
442    }
443
444    (x, a_seq, b_seq, c_seq)
445}
446
447fn compute_cost(x: &[Vector4<f64>], xref: &[Vector4<f64>], controls: &[Vector2<f64>]) -> f64 {
448    let q = state_cost_weight();
449    let qf = terminal_cost_weight();
450    let r = control_cost_weight();
451    let rd = control_rate_weight();
452
453    let mut cost = 0.0;
454    for t in 0..T {
455        cost += controls[t].dot(&(r * controls[t]));
456        if t != 0 {
457            let err = state_error(x[t], xref[t]);
458            cost += err.dot(&(q * err));
459        }
460        if t < T - 1 {
461            let du = controls[t + 1] - controls[t];
462            cost += du.dot(&(rd * du));
463        }
464    }
465
466    let terminal_error = state_error(x[T], xref[T]);
467    cost + terminal_error.dot(&(qf * terminal_error))
468}
469
470#[allow(clippy::needless_range_loop)]
471fn solve_qp_clarabel(
472    xref: &[Vector4<f64>],
473    xbar: &[Vector4<f64>],
474    state: &State,
475) -> Option<Vec<Vector2<f64>>> {
476    use clarabel::algebra::*;
477    use clarabel::solver::*;
478
479    let nu = 2;
480    let nx = 4;
481    let n_u = nu * T;
482    let n_x = nx * (T + 1);
483    let n = n_u + n_x;
484
485    let u_idx = |t: usize, k: usize| -> usize { t * nu + k };
486    let x_idx = |t: usize, k: usize| -> usize { n_u + t * nx + k };
487
488    // --- Build P (cost Hessian, upper triangular) ---
489    let mut p_rows = Vec::new();
490    let mut p_cols = Vec::new();
491    let mut p_vals = Vec::new();
492
493    for t in 0..T {
494        for k in 0..nu {
495            let idx = u_idx(t, k);
496            let mut val = R[k];
497            if t > 0 {
498                val += RD[k];
499            }
500            if t < T - 1 {
501                val += RD[k];
502            }
503            p_rows.push(idx);
504            p_cols.push(idx);
505            p_vals.push(val * 2.0);
506        }
507        if t < T - 1 {
508            for k in 0..nu {
509                let i = u_idx(t, k);
510                let j = u_idx(t + 1, k);
511                p_rows.push(i);
512                p_cols.push(j);
513                p_vals.push(-RD[k] * 2.0);
514            }
515        }
516    }
517
518    for t in 1..=T {
519        let w = if t == T { QF } else { Q };
520        for k in 0..nx {
521            let idx = x_idx(t, k);
522            p_rows.push(idx);
523            p_cols.push(idx);
524            p_vals.push(w[k] * 2.0);
525        }
526    }
527
528    let p = CscMatrix::new_from_triplets(n, n, p_rows, p_cols, p_vals);
529
530    // --- Build q (linear cost) ---
531    let mut q_vec = vec![0.0; n];
532    for t in 1..=T {
533        let w = if t == T { QF } else { Q };
534        for k in 0..nx {
535            q_vec[x_idx(t, k)] = -w[k] * 2.0 * xref[t][k];
536        }
537    }
538
539    // --- Build constraints ---
540    let n_eq = nx * (T + 1);
541    let n_steer_rate = T - 1;
542    let n_u_box = 2 * nu * T;
543    let n_steer_rate_box = 2 * n_steer_rate;
544    let n_v_box = 2 * (T + 1);
545    let n_ineq = n_u_box + n_steer_rate_box + n_v_box;
546
547    let mut a_rows = Vec::new();
548    let mut a_cols = Vec::new();
549    let mut a_vals = Vec::new();
550    let mut b_vec = vec![0.0; n_eq + n_ineq];
551
552    let x0 = state.to_vector();
553    for k in 0..nx {
554        a_rows.push(k);
555        a_cols.push(x_idx(0, k));
556        a_vals.push(1.0);
557        b_vec[k] = x0[k];
558    }
559
560    for t in 0..T {
561        let (a_mat, b_mat, c_vec) = get_linear_model_matrix(xbar[t][2], xbar[t][3], 0.0);
562        let base_row = nx * (t + 1);
563
564        for i in 0..nx {
565            let row = base_row + i;
566            a_rows.push(row);
567            a_cols.push(x_idx(t + 1, i));
568            a_vals.push(-1.0);
569            for j in 0..nx {
570                let val = a_mat[(i, j)];
571                if val.abs() > 1e-15 {
572                    a_rows.push(row);
573                    a_cols.push(x_idx(t, j));
574                    a_vals.push(val);
575                }
576            }
577            for j in 0..nu {
578                let val = b_mat[(i, j)];
579                if val.abs() > 1e-15 {
580                    a_rows.push(row);
581                    a_cols.push(u_idx(t, j));
582                    a_vals.push(val);
583                }
584            }
585            b_vec[row] = -c_vec[i];
586        }
587    }
588
589    let ineq_offset = n_eq;
590    let mut ineq_row = 0;
591
592    let u_bounds = [MAX_ACCEL, MAX_STEER];
593    for t in 0..T {
594        for (k, &bound) in u_bounds.iter().enumerate() {
595            let row = ineq_offset + ineq_row;
596            a_rows.push(row);
597            a_cols.push(u_idx(t, k));
598            a_vals.push(1.0);
599            b_vec[row] = bound;
600            ineq_row += 1;
601
602            let row = ineq_offset + ineq_row;
603            a_rows.push(row);
604            a_cols.push(u_idx(t, k));
605            a_vals.push(-1.0);
606            b_vec[row] = bound;
607            ineq_row += 1;
608        }
609    }
610
611    let max_steer_delta = MAX_DSTEER * DT;
612    for t in 0..(T - 1) {
613        let row = ineq_offset + ineq_row;
614        a_rows.push(row);
615        a_cols.push(u_idx(t + 1, 1));
616        a_vals.push(1.0);
617        a_rows.push(row);
618        a_cols.push(u_idx(t, 1));
619        a_vals.push(-1.0);
620        b_vec[row] = max_steer_delta;
621        ineq_row += 1;
622
623        let row = ineq_offset + ineq_row;
624        a_rows.push(row);
625        a_cols.push(u_idx(t + 1, 1));
626        a_vals.push(-1.0);
627        a_rows.push(row);
628        a_cols.push(u_idx(t, 1));
629        a_vals.push(1.0);
630        b_vec[row] = max_steer_delta;
631        ineq_row += 1;
632    }
633
634    for t in 0..=T {
635        let row = ineq_offset + ineq_row;
636        a_rows.push(row);
637        a_cols.push(x_idx(t, 2));
638        a_vals.push(1.0);
639        b_vec[row] = MAX_SPEED;
640        ineq_row += 1;
641
642        let row = ineq_offset + ineq_row;
643        a_rows.push(row);
644        a_cols.push(x_idx(t, 2));
645        a_vals.push(-1.0);
646        b_vec[row] = -MIN_SPEED;
647        ineq_row += 1;
648    }
649
650    let m = n_eq + n_ineq;
651    let a_csc = CscMatrix::new_from_triplets(m, n, a_rows, a_cols, a_vals);
652
653    let cones = vec![
654        SupportedConeT::ZeroConeT(n_eq),
655        SupportedConeT::NonnegativeConeT(n_ineq),
656    ];
657
658    let settings = DefaultSettingsBuilder::default()
659        .verbose(false)
660        .build()
661        // Default settings builder always succeeds
662        .expect("DefaultSettingsBuilder with defaults should not fail");
663
664    let mut solver = DefaultSolver::new(&p, &q_vec, &a_csc, &b_vec, &cones, settings).ok()?;
665    solver.solve();
666
667    if solver.solution.status != SolverStatus::Solved {
668        return None;
669    }
670
671    let z = &solver.solution.x;
672    let controls: Vec<Vector2<f64>> = (0..T)
673        .map(|t| Vector2::new(z[u_idx(t, 0)], z[u_idx(t, 1)]))
674        .collect();
675
676    Some(controls)
677}
678
679fn optimize_linearized_controls(
680    xref: &[Vector4<f64>],
681    xbar: &[Vector4<f64>],
682    state: &State,
683    initial_controls: &[Vector2<f64>],
684) -> Vec<Vector2<f64>> {
685    let pg_controls = optimize_linearized_controls_pg(xref, xbar, state, initial_controls);
686
687    // Adaptive solver selection: only invoke QP solver during reverse maneuvers
688    // where the projected-gradient solver struggles with the nonlinear coupling.
689    // During forward driving, PG alone is sufficient and faster.
690    let is_reverse = state.v < -0.1 || xref.iter().any(|x| x[2] < -0.1);
691    if is_reverse {
692        if let Some(qp_controls) = solve_qp_clarabel(xref, xbar, state) {
693            let (qp_x, _, _, _) = linearized_rollout(state, xbar, &qp_controls);
694            let qp_cost = compute_cost(&qp_x, xref, &qp_controls);
695
696            let (pg_x, _, _, _) = linearized_rollout(state, xbar, &pg_controls);
697            let pg_cost = compute_cost(&pg_x, xref, &pg_controls);
698
699            if qp_cost <= pg_cost {
700                return qp_controls;
701            }
702        }
703    }
704
705    pg_controls
706}
707
708fn optimize_linearized_controls_pg(
709    xref: &[Vector4<f64>],
710    xbar: &[Vector4<f64>],
711    state: &State,
712    initial_controls: &[Vector2<f64>],
713) -> Vec<Vector2<f64>> {
714    let q = state_cost_weight();
715    let qf = terminal_cost_weight();
716    let r = control_cost_weight();
717    let rd = control_rate_weight();
718
719    let mut controls = initial_controls.to_vec();
720    controls.resize(T, Vector2::zeros());
721    apply_control_constraints(&mut controls);
722
723    let mut previous_cost = f64::INFINITY;
724
725    for _ in 0..QP_MAX_ITERS {
726        let (x, a_seq, b_seq, _) = linearized_rollout(state, xbar, &controls);
727        let current_cost = compute_cost(&x, xref, &controls);
728
729        let mut gradients = [Vector2::zeros(); T];
730        let mut lambda = (qf * state_error(x[T], xref[T])) * 2.0;
731
732        for t in (0..T).rev() {
733            let mut grad = (r * controls[t]) * 2.0 + b_seq[t].transpose() * lambda;
734
735            if t > 0 {
736                grad += (rd * (controls[t] - controls[t - 1])) * 2.0;
737            }
738            if t < T - 1 {
739                grad -= (rd * (controls[t + 1] - controls[t])) * 2.0;
740            }
741
742            gradients[t] = grad;
743
744            let lx = if t == 0 {
745                Vector4::zeros()
746            } else {
747                (q * state_error(x[t], xref[t])) * 2.0
748            };
749            lambda = lx + a_seq[t].transpose() * lambda;
750        }
751
752        let gradient_norm = gradients
753            .iter()
754            .map(Vector2::norm_squared)
755            .sum::<f64>()
756            .sqrt();
757
758        if gradient_norm <= GRAD_TOL || (previous_cost - current_cost).abs() <= COST_TOL {
759            break;
760        }
761
762        previous_cost = current_cost;
763        let current_controls = controls.clone();
764        let mut best_candidate_controls = None;
765        let mut best_candidate_cost = current_cost;
766
767        let mut step = LINE_SEARCH_INITIAL_STEP;
768        for &large_step in &[1.0, 0.5] {
769            let mut candidate = current_controls.clone();
770            for t in 0..T {
771                candidate[t] -= gradients[t] * large_step;
772            }
773            apply_control_constraints(&mut candidate);
774
775            let (candidate_x, _, _, _) = linearized_rollout(state, xbar, &candidate);
776            let candidate_cost = compute_cost(&candidate_x, xref, &candidate);
777            if candidate_cost < best_candidate_cost {
778                best_candidate_cost = candidate_cost;
779                best_candidate_controls = Some(candidate);
780            }
781        }
782
783        for _ in 0..LINE_SEARCH_ITERS {
784            let mut candidate = current_controls.clone();
785            for t in 0..T {
786                candidate[t] -= gradients[t] * step;
787            }
788            apply_control_constraints(&mut candidate);
789
790            let (candidate_x, _, _, _) = linearized_rollout(state, xbar, &candidate);
791            let candidate_cost = compute_cost(&candidate_x, xref, &candidate);
792            if candidate_cost < best_candidate_cost {
793                best_candidate_cost = candidate_cost;
794                best_candidate_controls = Some(candidate);
795            }
796
797            step *= 0.5;
798        }
799
800        if let Some(candidate) = best_candidate_controls {
801            controls = candidate;
802        } else {
803            break;
804        }
805    }
806
807    controls
808}
809
810pub fn iterative_linear_mpc_control(
811    xref: &[Vector4<f64>],
812    state: &State,
813    warm_start: &[Vector2<f64>],
814) -> MpcResult {
815    let mut controls = warm_start.to_vec();
816    controls.resize(T, Vector2::zeros());
817    apply_control_constraints(&mut controls);
818
819    let mut predicted = predict_motion(*state, &controls);
820    for _ in 0..MAX_ITER {
821        let previous_controls = controls.clone();
822        controls = optimize_linearized_controls(xref, &predicted, state, &controls);
823        predicted = predict_motion(*state, &controls);
824
825        let du = controls
826            .iter()
827            .zip(previous_controls.iter())
828            .map(|(current, previous)| {
829                (current[0] - previous[0]).abs() + (current[1] - previous[1]).abs()
830            })
831            .sum::<f64>();
832
833        if du <= DU_TH {
834            break;
835        }
836    }
837
838    MpcResult {
839        controls,
840        predicted,
841    }
842}
843
844fn next_warm_start(controls: &[Vector2<f64>]) -> Vec<Vector2<f64>> {
845    let mut warm_start = controls[1..].to_vec();
846    warm_start.push(controls.last().copied().unwrap_or(Vector2::zeros()));
847    warm_start
848}
849
850pub fn check_goal(
851    state: &State,
852    goal: (f64, f64),
853    target_index: usize,
854    final_index: usize,
855) -> bool {
856    let dx = state.x - goal.0;
857    let dy = state.y - goal.1;
858    let distance = (dx * dx + dy * dy).sqrt();
859
860    let near_goal = distance <= GOAL_DIS;
861    let near_end = final_index.abs_diff(target_index) < 5;
862    let stopped = state.v.abs() <= STOP_SPEED;
863
864    near_goal && near_end && stopped
865}
866
867fn sample_reference_course(
868    ax: &[f64],
869    ay: &[f64],
870    course_tick: f64,
871) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
872    let csp = CubicSpline2D::new(ax, ay);
873    let s_max = *csp.s.last().unwrap_or(&0.0);
874
875    let mut cx = Vec::new();
876    let mut cy = Vec::new();
877    let mut cyaw = Vec::new();
878
879    let mut s = 0.0;
880    while s <= s_max {
881        let (x, y) = csp.calc_position(s);
882        cx.push(x);
883        cy.push(y);
884        cyaw.push(csp.calc_yaw(s));
885        s += course_tick;
886    }
887
888    if cx.is_empty() || cy.is_empty() || cyaw.is_empty() {
889        cx.push(ax[0]);
890        cy.push(ay[0]);
891        cyaw.push(0.0);
892    }
893
894    (cx, cy, cyaw)
895}
896
897pub fn generate_reference_course(ax: &[f64], ay: &[f64]) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
898    let (cx, cy, mut cyaw) = sample_reference_course(ax, ay, PATH_RESOLUTION);
899
900    smooth_yaw(&mut cyaw);
901    (cx, cy, cyaw)
902}
903
904fn generate_switch_back_course() -> (Vec<f64>, Vec<f64>, Vec<f64>) {
905    let (mut cx, mut cy, mut cyaw) = sample_reference_course(
906        &[0.0, 30.0, 6.0, 20.0, 35.0],
907        &[0.0, 0.0, 20.0, 35.0, 20.0],
908        UPSTREAM_SWITCH_BACK_TICK,
909    );
910    let (cx2, cy2, mut cyaw2) = sample_reference_course(
911        &[35.0, 10.0, 0.0, 0.0],
912        &[20.0, 30.0, 5.0, 0.0],
913        UPSTREAM_SWITCH_BACK_TICK,
914    );
915
916    for yaw in &mut cyaw2 {
917        *yaw -= PI;
918    }
919
920    cx.extend(cx2);
921    cy.extend(cy2);
922    cyaw.extend(cyaw2);
923    smooth_yaw(&mut cyaw);
924
925    (cx, cy, cyaw)
926}
927
928pub fn run_mpc_simulation() -> SimulationResult {
929    let (cx, cy, cyaw) = generate_switch_back_course();
930    run_mpc_simulation_with_reference(cx, cy, cyaw, UPSTREAM_SWITCH_BACK_TICK, MAX_SIM_STEPS)
931}
932
933fn run_mpc_simulation_with_course(
934    ax: &[f64],
935    ay: &[f64],
936    max_sim_steps: usize,
937) -> SimulationResult {
938    let (cx, cy, cyaw) = generate_reference_course(ax, ay);
939    run_mpc_simulation_with_reference(cx, cy, cyaw, PATH_RESOLUTION, max_sim_steps)
940}
941
942fn run_mpc_simulation_with_reference(
943    cx: Vec<f64>,
944    cy: Vec<f64>,
945    cyaw: Vec<f64>,
946    course_tick: f64,
947    max_sim_steps: usize,
948) -> SimulationResult {
949    let speed_profile = calc_speed_profile(&cx, &cy, &cyaw, TARGET_SPEED);
950
951    let mut state = State::new(cx[0], cy[0], 0.0, cyaw[0]);
952    if state.yaw - cyaw[0] >= PI {
953        state.yaw -= 2.0 * PI;
954    } else if state.yaw - cyaw[0] <= -PI {
955        state.yaw += 2.0 * PI;
956    }
957
958    let mut target_index = 0;
959    let final_index = cx.len() - 1;
960    let goal = (cx[final_index], cy[final_index]);
961
962    let mut warm_start = vec![Vector2::zeros(); T];
963
964    let mut hist_x = vec![state.x];
965    let mut hist_y = vec![state.y];
966    let mut predicted = vec![state.to_vector()];
967    let mut reached_goal = false;
968
969    for _ in 0..max_sim_steps {
970        let (xref, new_index) = calc_ref_trajectory(
971            &state,
972            &cx,
973            &cy,
974            &cyaw,
975            &speed_profile,
976            course_tick,
977            target_index,
978        );
979        target_index = new_index;
980
981        let mpc_result = iterative_linear_mpc_control(&xref, &state, &warm_start);
982        let accel = mpc_result.controls[0][0];
983        let steer = mpc_result.controls[0][1];
984
985        state.update(accel, steer);
986        hist_x.push(state.x);
987        hist_y.push(state.y);
988        predicted = mpc_result.predicted;
989
990        warm_start = next_warm_start(&mpc_result.controls);
991
992        if check_goal(&state, goal, target_index, final_index) {
993            reached_goal = true;
994            break;
995        }
996    }
997
998    SimulationResult {
999        cx,
1000        cy,
1001        hist_x,
1002        hist_y,
1003        predicted_x: predicted.iter().map(|p| p[0]).collect(),
1004        predicted_y: predicted.iter().map(|p| p[1]).collect(),
1005        goal,
1006        reached_goal,
1007        target_index,
1008        final_index,
1009        final_state: state,
1010    }
1011}
1012
1013#[cfg(test)]
1014mod tests {
1015    use super::*;
1016    use std::collections::BTreeMap;
1017
1018    /// Original (pre-promotion) solver using first-improvement backtracking.
1019    /// Kept for characterization tests that compare default vs expanded behavior.
1020    fn optimize_linearized_controls_original(
1021        xref: &[Vector4<f64>],
1022        xbar: &[Vector4<f64>],
1023        state: &State,
1024        initial_controls: &[Vector2<f64>],
1025    ) -> Vec<Vector2<f64>> {
1026        let q = state_cost_weight();
1027        let qf = terminal_cost_weight();
1028        let r = control_cost_weight();
1029        let rd = control_rate_weight();
1030
1031        let mut controls = initial_controls.to_vec();
1032        controls.resize(T, Vector2::zeros());
1033        apply_control_constraints(&mut controls);
1034
1035        let mut previous_cost = f64::INFINITY;
1036
1037        for _ in 0..QP_MAX_ITERS {
1038            let (x, a_seq, b_seq, _) = linearized_rollout(state, xbar, &controls);
1039            let current_cost = compute_cost(&x, xref, &controls);
1040
1041            let mut gradients = [Vector2::zeros(); T];
1042            let mut lambda = (qf * state_error(x[T], xref[T])) * 2.0;
1043
1044            for t in (0..T).rev() {
1045                let mut grad = (r * controls[t]) * 2.0 + b_seq[t].transpose() * lambda;
1046
1047                if t > 0 {
1048                    grad += (rd * (controls[t] - controls[t - 1])) * 2.0;
1049                }
1050                if t < T - 1 {
1051                    grad -= (rd * (controls[t + 1] - controls[t])) * 2.0;
1052                }
1053
1054                gradients[t] = grad;
1055
1056                let lx = if t == 0 {
1057                    Vector4::zeros()
1058                } else {
1059                    (q * state_error(x[t], xref[t])) * 2.0
1060                };
1061                lambda = lx + a_seq[t].transpose() * lambda;
1062            }
1063
1064            let gradient_norm = gradients
1065                .iter()
1066                .map(Vector2::norm_squared)
1067                .sum::<f64>()
1068                .sqrt();
1069
1070            if gradient_norm <= GRAD_TOL || (previous_cost - current_cost).abs() <= COST_TOL {
1071                break;
1072            }
1073
1074            previous_cost = current_cost;
1075            let current_controls = controls.clone();
1076            let mut step = LINE_SEARCH_INITIAL_STEP;
1077            let mut improved = false;
1078
1079            for _ in 0..LINE_SEARCH_ITERS {
1080                let mut candidate = current_controls.clone();
1081                for t in 0..T {
1082                    candidate[t] -= gradients[t] * step;
1083                }
1084                apply_control_constraints(&mut candidate);
1085
1086                let (candidate_x, _, _, _) = linearized_rollout(state, xbar, &candidate);
1087                let candidate_cost = compute_cost(&candidate_x, xref, &candidate);
1088                if candidate_cost < current_cost {
1089                    controls = candidate;
1090                    improved = true;
1091                    break;
1092                }
1093
1094                step *= 0.5;
1095            }
1096
1097            if !improved {
1098                break;
1099            }
1100        }
1101
1102        controls
1103    }
1104
1105    #[derive(Debug)]
1106    struct SwitchBackTraceRow {
1107        step: usize,
1108        target_index: usize,
1109        state: Vector4<f64>,
1110        xref_head: Vector4<f64>,
1111        xref_tail: Vector4<f64>,
1112        control: Vector2<f64>,
1113        predicted_tail: Vector4<f64>,
1114    }
1115
1116    #[derive(Debug)]
1117    struct SwitchBackDetailedRow {
1118        step: usize,
1119        target_index: usize,
1120        state: Vector4<f64>,
1121        xref: Vec<Vector4<f64>>,
1122        controls: Vec<Vector2<f64>>,
1123        predicted: Vec<Vector4<f64>>,
1124    }
1125
1126    #[derive(Debug)]
1127    struct SwitchBackPlanningSnapshot {
1128        step: usize,
1129        target_index: usize,
1130        state: Vector4<f64>,
1131        xref_head: Vector4<f64>,
1132        warm_start_head: Vector2<f64>,
1133    }
1134
1135    #[derive(Debug)]
1136    struct SwitchBackClosedLoopCheckpoint {
1137        step: usize,
1138        target_index: usize,
1139        goal_distance: f64,
1140        state: Vector4<f64>,
1141    }
1142
1143    #[derive(Debug)]
1144    struct SwitchBackGoalConditionSnapshot {
1145        step: usize,
1146        target_index: usize,
1147        goal_distance: f64,
1148        speed: f64,
1149        speed_abs: f64,
1150        heading_error: f64,
1151        motion_curvature: f64,
1152        near_goal: bool,
1153        near_end: bool,
1154        stopped: bool,
1155        reached_goal: bool,
1156    }
1157
1158    #[derive(Debug)]
1159    struct SwitchBackGoalControlSnapshot {
1160        step: usize,
1161        target_index: usize,
1162        goal_distance: f64,
1163        speed: f64,
1164        accel: f64,
1165        steer: f64,
1166        heading_error: f64,
1167        motion_curvature: f64,
1168        course_curvature: f64,
1169        near_goal: bool,
1170        near_end: bool,
1171        stopped: bool,
1172        reached_goal: bool,
1173    }
1174
1175    type SwitchBackControlTrace = BTreeMap<usize, Vec<Vector2<f64>>>;
1176    type SwitchBackStateTrace = BTreeMap<usize, Vec<Vector4<f64>>>;
1177    type SwitchBackHorizonPair = (Vec<Vector4<f64>>, Vec<Vector4<f64>>);
1178    type SwitchBackHorizonTrace = BTreeMap<usize, SwitchBackHorizonPair>;
1179    type SwitchBackWarmStartTrace = BTreeMap<usize, Vec<Vector2<f64>>>;
1180
1181    #[derive(Debug, Default)]
1182    struct StateInjectionMetrics {
1183        default_same_control0: f64,
1184        default_opposite_control0: f64,
1185        default_same_pred_y: f64,
1186        default_opposite_pred_y: f64,
1187        default_same_pred_v: f64,
1188        default_opposite_pred_v: f64,
1189        expanded_same_control0: f64,
1190        expanded_opposite_control0: f64,
1191        expanded_same_pred_y: f64,
1192        expanded_opposite_pred_y: f64,
1193        expanded_same_pred_v: f64,
1194        expanded_opposite_pred_v: f64,
1195    }
1196
1197    struct SwitchBackContext {
1198        cx: Vec<f64>,
1199        cy: Vec<f64>,
1200        cyaw: Vec<f64>,
1201        speed_profile: Vec<f64>,
1202        state: State,
1203        target_index: usize,
1204        warm_start: Vec<Vector2<f64>>,
1205    }
1206
1207    #[derive(Debug)]
1208    struct SwitchBackTraceDiffSummary {
1209        max_target_index_gap: usize,
1210        max_state_diff: [f64; 4],
1211        max_xref_head_diff: [f64; 4],
1212        max_xref_tail_diff: [f64; 4],
1213        max_control_diff: [f64; 2],
1214        max_predicted_tail_diff: [f64; 4],
1215    }
1216
1217    #[derive(Debug)]
1218    struct SwitchBackDetailedDiffSummary {
1219        max_target_index_gap: usize,
1220        max_state_diff: [f64; 4],
1221        max_xref_diff: [f64; 4],
1222        max_control_diff: [f64; 2],
1223        max_predicted_diff: [f64; 4],
1224    }
1225
1226    fn assert_vec2_close(actual: &Vector2<f64>, expected: [f64; 2], tol: f64) {
1227        for (i, expected_value) in expected.iter().enumerate() {
1228            assert!(
1229                (actual[i] - expected_value).abs() <= tol,
1230                "index {}: actual={} expected={} tol={}",
1231                i,
1232                actual[i],
1233                expected_value,
1234                tol
1235            );
1236        }
1237    }
1238
1239    fn assert_vec4_close(actual: &Vector4<f64>, expected: [f64; 4], tol: f64) {
1240        for (i, expected_value) in expected.iter().enumerate() {
1241            assert!(
1242                (actual[i] - expected_value).abs() <= tol,
1243                "index {}: actual={} expected={} tol={}",
1244                i,
1245                actual[i],
1246                expected_value,
1247                tol
1248            );
1249        }
1250    }
1251
1252    fn parse_switch_back_trace(csv: &str) -> Vec<SwitchBackTraceRow> {
1253        csv.lines()
1254            .skip(1)
1255            .filter(|line| !line.trim().is_empty())
1256            .map(|line| {
1257                let values: Vec<&str> = line.split(',').collect();
1258                assert_eq!(values.len(), 20);
1259
1260                SwitchBackTraceRow {
1261                    step: values[0].parse().unwrap(),
1262                    target_index: values[1].parse().unwrap(),
1263                    state: Vector4::new(
1264                        values[2].parse().unwrap(),
1265                        values[3].parse().unwrap(),
1266                        values[4].parse().unwrap(),
1267                        values[5].parse().unwrap(),
1268                    ),
1269                    xref_head: Vector4::new(
1270                        values[6].parse().unwrap(),
1271                        values[7].parse().unwrap(),
1272                        values[8].parse().unwrap(),
1273                        values[9].parse().unwrap(),
1274                    ),
1275                    xref_tail: Vector4::new(
1276                        values[10].parse().unwrap(),
1277                        values[11].parse().unwrap(),
1278                        values[12].parse().unwrap(),
1279                        values[13].parse().unwrap(),
1280                    ),
1281                    control: Vector2::new(values[14].parse().unwrap(), values[15].parse().unwrap()),
1282                    predicted_tail: Vector4::new(
1283                        values[16].parse().unwrap(),
1284                        values[17].parse().unwrap(),
1285                        values[18].parse().unwrap(),
1286                        values[19].parse().unwrap(),
1287                    ),
1288                }
1289            })
1290            .collect()
1291    }
1292
1293    fn parse_switch_back_reverse_controls(csv: &str) -> SwitchBackControlTrace {
1294        let mut result: SwitchBackControlTrace = BTreeMap::new();
1295
1296        for line in csv.lines().skip(1).filter(|line| !line.trim().is_empty()) {
1297            let values: Vec<&str> = line.split(',').collect();
1298            assert_eq!(values.len(), 4);
1299            let step: usize = values[0].parse().unwrap();
1300            let control_index: usize = values[1].parse().unwrap();
1301            let control = Vector2::new(values[2].parse().unwrap(), values[3].parse().unwrap());
1302
1303            let controls = result.entry(step).or_default();
1304            assert_eq!(controls.len(), control_index);
1305            controls.push(control);
1306        }
1307
1308        result
1309    }
1310
1311    fn parse_switch_back_reverse_states(csv: &str) -> SwitchBackStateTrace {
1312        let mut result: SwitchBackStateTrace = BTreeMap::new();
1313
1314        for line in csv.lines().skip(1).filter(|line| !line.trim().is_empty()) {
1315            let values: Vec<&str> = line.split(',').collect();
1316            assert_eq!(values.len(), 6);
1317            let step: usize = values[0].parse().unwrap();
1318            let horizon_index: usize = values[1].parse().unwrap();
1319            let state = Vector4::new(
1320                values[2].parse().unwrap(),
1321                values[3].parse().unwrap(),
1322                values[4].parse().unwrap(),
1323                values[5].parse().unwrap(),
1324            );
1325
1326            let states = result.entry(step).or_default();
1327            assert_eq!(states.len(), horizon_index);
1328            states.push(state);
1329        }
1330
1331        result
1332    }
1333
1334    fn parse_switch_back_reverse_horizon(csv: &str) -> SwitchBackHorizonTrace {
1335        let mut result: SwitchBackHorizonTrace = BTreeMap::new();
1336
1337        for line in csv.lines().skip(1).filter(|line| !line.trim().is_empty()) {
1338            let values: Vec<&str> = line.split(',').collect();
1339            assert_eq!(values.len(), 10);
1340            let step: usize = values[0].parse().unwrap();
1341            let horizon_index: usize = values[1].parse().unwrap();
1342
1343            let entry = result.entry(step).or_default();
1344            assert_eq!(entry.0.len(), horizon_index);
1345            assert_eq!(entry.1.len(), horizon_index);
1346
1347            entry.0.push(Vector4::new(
1348                values[2].parse().unwrap(),
1349                values[3].parse().unwrap(),
1350                values[4].parse().unwrap(),
1351                values[5].parse().unwrap(),
1352            ));
1353            entry.1.push(Vector4::new(
1354                values[6].parse().unwrap(),
1355                values[7].parse().unwrap(),
1356                values[8].parse().unwrap(),
1357                values[9].parse().unwrap(),
1358            ));
1359        }
1360
1361        result
1362    }
1363
1364    fn trace_state_diff(actual: &Vector4<f64>, expected: &Vector4<f64>) -> [f64; 4] {
1365        [
1366            (actual[0] - expected[0]).abs(),
1367            (actual[1] - expected[1]).abs(),
1368            (actual[2] - expected[2]).abs(),
1369            angle_diff(actual[3], expected[3]).abs(),
1370        ]
1371    }
1372
1373    fn trace_control_diff(actual: &Vector2<f64>, expected: &Vector2<f64>) -> [f64; 2] {
1374        [
1375            (actual[0] - expected[0]).abs(),
1376            (actual[1] - expected[1]).abs(),
1377        ]
1378    }
1379
1380    fn summarize_switch_back_trace_window(
1381        actual: &[SwitchBackTraceRow],
1382        expected: &[SwitchBackTraceRow],
1383    ) -> SwitchBackTraceDiffSummary {
1384        let mut summary = SwitchBackTraceDiffSummary {
1385            max_target_index_gap: 0,
1386            max_state_diff: [0.0; 4],
1387            max_xref_head_diff: [0.0; 4],
1388            max_xref_tail_diff: [0.0; 4],
1389            max_control_diff: [0.0; 2],
1390            max_predicted_tail_diff: [0.0; 4],
1391        };
1392
1393        assert_eq!(actual.len(), expected.len());
1394
1395        for (actual_row, expected_row) in actual.iter().zip(expected.iter()) {
1396            assert_eq!(actual_row.step, expected_row.step);
1397            summary.max_target_index_gap = summary
1398                .max_target_index_gap
1399                .max(actual_row.target_index.abs_diff(expected_row.target_index));
1400
1401            for (max_diff, diff) in summary
1402                .max_state_diff
1403                .iter_mut()
1404                .zip(trace_state_diff(&actual_row.state, &expected_row.state))
1405            {
1406                *max_diff = (*max_diff).max(diff);
1407            }
1408
1409            for (max_diff, diff) in summary.max_xref_head_diff.iter_mut().zip(trace_state_diff(
1410                &actual_row.xref_head,
1411                &expected_row.xref_head,
1412            )) {
1413                *max_diff = (*max_diff).max(diff);
1414            }
1415
1416            for (max_diff, diff) in summary.max_xref_tail_diff.iter_mut().zip(trace_state_diff(
1417                &actual_row.xref_tail,
1418                &expected_row.xref_tail,
1419            )) {
1420                *max_diff = (*max_diff).max(diff);
1421            }
1422
1423            for (max_diff, diff) in summary.max_control_diff.iter_mut().zip(trace_control_diff(
1424                &actual_row.control,
1425                &expected_row.control,
1426            )) {
1427                *max_diff = (*max_diff).max(diff);
1428            }
1429
1430            for (max_diff, diff) in
1431                summary
1432                    .max_predicted_tail_diff
1433                    .iter_mut()
1434                    .zip(trace_state_diff(
1435                        &actual_row.predicted_tail,
1436                        &expected_row.predicted_tail,
1437                    ))
1438            {
1439                *max_diff = (*max_diff).max(diff);
1440            }
1441        }
1442
1443        summary
1444    }
1445
1446    fn summarize_switch_back_detailed_window(
1447        actual_trace: &[SwitchBackDetailedRow],
1448        expected_trace: &[SwitchBackTraceRow],
1449        expected_controls: &SwitchBackControlTrace,
1450        expected_horizon: &SwitchBackHorizonTrace,
1451        step_window: std::ops::RangeInclusive<usize>,
1452    ) -> SwitchBackDetailedDiffSummary {
1453        let mut summary = SwitchBackDetailedDiffSummary {
1454            max_target_index_gap: 0,
1455            max_state_diff: [0.0; 4],
1456            max_xref_diff: [0.0; 4],
1457            max_control_diff: [0.0; 2],
1458            max_predicted_diff: [0.0; 4],
1459        };
1460
1461        for actual_row in actual_trace
1462            .iter()
1463            .filter(|row| step_window.contains(&row.step))
1464        {
1465            let expected_row = &expected_trace[actual_row.step];
1466            assert_eq!(actual_row.step, expected_row.step);
1467            summary.max_target_index_gap = summary
1468                .max_target_index_gap
1469                .max(actual_row.target_index.abs_diff(expected_row.target_index));
1470
1471            for (slot, diff) in summary
1472                .max_state_diff
1473                .iter_mut()
1474                .zip(trace_state_diff(&actual_row.state, &expected_row.state))
1475            {
1476                *slot = (*slot).max(diff);
1477            }
1478
1479            let expected_controls_for_step = expected_controls.get(&actual_row.step).unwrap();
1480            let (expected_xref_for_step, expected_predicted_for_step) =
1481                expected_horizon.get(&actual_row.step).unwrap();
1482            let expected_xref_rows = expected_xref_for_step
1483                .iter()
1484                .map(|vec| [vec[0], vec[1], vec[2], vec[3]])
1485                .collect::<Vec<_>>();
1486            let expected_control_rows = expected_controls_for_step
1487                .iter()
1488                .map(|vec| [vec[0], vec[1]])
1489                .collect::<Vec<_>>();
1490            let expected_predicted_rows = expected_predicted_for_step
1491                .iter()
1492                .map(|vec| [vec[0], vec[1], vec[2], vec[3]])
1493                .collect::<Vec<_>>();
1494
1495            for (slot, diff) in summary
1496                .max_xref_diff
1497                .iter_mut()
1498                .zip(summarize_vec4_sequence_diff(
1499                    &actual_row.xref,
1500                    &expected_xref_rows,
1501                ))
1502            {
1503                *slot = (*slot).max(diff);
1504            }
1505
1506            for (slot, diff) in
1507                summary
1508                    .max_control_diff
1509                    .iter_mut()
1510                    .zip(summarize_vec2_sequence_diff(
1511                        &actual_row.controls,
1512                        &expected_control_rows,
1513                    ))
1514            {
1515                *slot = (*slot).max(diff);
1516            }
1517
1518            for (slot, diff) in
1519                summary
1520                    .max_predicted_diff
1521                    .iter_mut()
1522                    .zip(summarize_vec4_sequence_diff(
1523                        &actual_row.predicted,
1524                        &expected_predicted_rows,
1525                    ))
1526            {
1527                *slot = (*slot).max(diff);
1528            }
1529        }
1530
1531        summary
1532    }
1533
1534    fn assert_diff_bounds<const N: usize>(label: &str, actual: [f64; N], tolerance: [f64; N]) {
1535        for (i, (actual_diff, bound)) in actual.into_iter().zip(tolerance).enumerate() {
1536            assert!(
1537                actual_diff <= bound,
1538                "{} diff index {}: actual={} tolerance={}",
1539                label,
1540                i,
1541                actual_diff,
1542                bound
1543            );
1544        }
1545    }
1546
1547    fn summarize_vec4_sequence_diff(actual: &[Vector4<f64>], expected: &[[f64; 4]]) -> [f64; 4] {
1548        assert_eq!(actual.len(), expected.len());
1549        let mut max_diff: [f64; 4] = [0.0; 4];
1550
1551        for (actual_vec, expected_vec) in actual.iter().zip(expected.iter()) {
1552            for (slot, diff) in max_diff.iter_mut().zip(trace_state_diff(
1553                actual_vec,
1554                &Vector4::from_row_slice(expected_vec),
1555            )) {
1556                *slot = (*slot).max(diff);
1557            }
1558        }
1559
1560        max_diff
1561    }
1562
1563    fn summarize_vec2_sequence_diff(actual: &[Vector2<f64>], expected: &[[f64; 2]]) -> [f64; 2] {
1564        assert_eq!(actual.len(), expected.len());
1565        let mut max_diff: [f64; 2] = [0.0; 2];
1566
1567        for (actual_vec, expected_vec) in actual.iter().zip(expected.iter()) {
1568            for (slot, diff) in max_diff.iter_mut().zip(trace_control_diff(
1569                actual_vec,
1570                &Vector2::from_row_slice(expected_vec),
1571            )) {
1572                *slot = (*slot).max(diff);
1573            }
1574        }
1575
1576        max_diff
1577    }
1578
1579    fn compute_cost_without_angle_wrap(
1580        x: &[Vector4<f64>],
1581        xref: &[Vector4<f64>],
1582        controls: &[Vector2<f64>],
1583    ) -> f64 {
1584        let q = state_cost_weight();
1585        let qf = terminal_cost_weight();
1586        let r = control_cost_weight();
1587        let rd = control_rate_weight();
1588
1589        let mut cost = 0.0;
1590        for t in 0..T {
1591            cost += controls[t].dot(&(r * controls[t]));
1592            if t != 0 {
1593                let err = x[t] - xref[t];
1594                cost += err.dot(&(q * err));
1595            }
1596            if t < T - 1 {
1597                let du = controls[t + 1] - controls[t];
1598                cost += du.dot(&(rd * du));
1599            }
1600        }
1601
1602        let terminal_error = x[T] - xref[T];
1603        cost + terminal_error.dot(&(qf * terminal_error))
1604    }
1605
1606    #[derive(Debug)]
1607    struct GradientTraceEntry {
1608        iter: usize,
1609        cost: f64,
1610        gradient_norm: f64,
1611        accepted_step: Option<f64>,
1612    }
1613
1614    #[derive(Debug)]
1615    struct LineSearchProjectionEntry {
1616        step: f64,
1617        raw_cost: f64,
1618        projected_cost: f64,
1619        raw_to_projected: [f64; 2],
1620        raw_to_python: [f64; 2],
1621        projected_to_python: [f64; 2],
1622    }
1623
1624    #[derive(Debug)]
1625    struct ProjectionTraceEntry {
1626        iter: usize,
1627        current_cost: f64,
1628        candidates: Vec<LineSearchProjectionEntry>,
1629    }
1630
1631    fn control_sequence_max_diff(actual: &[Vector2<f64>], expected: &[Vector2<f64>]) -> [f64; 2] {
1632        assert_eq!(actual.len(), expected.len());
1633        let mut max_diff: [f64; 2] = [0.0; 2];
1634
1635        for (actual_vec, expected_vec) in actual.iter().zip(expected.iter()) {
1636            let diff = trace_control_diff(actual_vec, expected_vec);
1637            max_diff[0] = max_diff[0].max(diff[0]);
1638            max_diff[1] = max_diff[1].max(diff[1]);
1639        }
1640
1641        max_diff
1642    }
1643
1644    fn linearized_rollout_without_angle_wrap(
1645        x0: &State,
1646        xbar: &[Vector4<f64>],
1647        controls: &[Vector2<f64>],
1648    ) -> Vec<Vector4<f64>> {
1649        let mut x = vec![Vector4::zeros(); T + 1];
1650        x[0] = x0.to_vector();
1651
1652        for t in 0..T {
1653            let (a, b, c) = get_linear_model_matrix(xbar[t][2], xbar[t][3], 0.0);
1654            x[t + 1] = a * x[t] + b * controls[t] + c;
1655        }
1656
1657        x
1658    }
1659
1660    fn optimize_linearized_controls_without_angle_wrap_with_trace(
1661        xref: &[Vector4<f64>],
1662        xbar: &[Vector4<f64>],
1663        state: &State,
1664        initial_controls: &[Vector2<f64>],
1665        max_iters: usize,
1666        line_search_iters: usize,
1667    ) -> (Vec<Vector2<f64>>, Vec<GradientTraceEntry>) {
1668        optimize_linearized_controls_without_angle_wrap_with_trace_and_initial_step(
1669            xref,
1670            xbar,
1671            state,
1672            initial_controls,
1673            max_iters,
1674            line_search_iters,
1675            0.25,
1676        )
1677    }
1678
1679    fn optimize_linearized_controls_without_angle_wrap_with_trace_and_initial_step(
1680        xref: &[Vector4<f64>],
1681        xbar: &[Vector4<f64>],
1682        state: &State,
1683        initial_controls: &[Vector2<f64>],
1684        max_iters: usize,
1685        line_search_iters: usize,
1686        initial_step: f64,
1687    ) -> (Vec<Vector2<f64>>, Vec<GradientTraceEntry>) {
1688        let q = state_cost_weight();
1689        let qf = terminal_cost_weight();
1690        let r = control_cost_weight();
1691        let rd = control_rate_weight();
1692
1693        let mut controls = initial_controls.to_vec();
1694        controls.resize(T, Vector2::zeros());
1695        apply_control_constraints(&mut controls);
1696
1697        let mut previous_cost = f64::INFINITY;
1698        let mut trace = Vec::new();
1699
1700        for iter in 0..max_iters {
1701            let mut x = vec![Vector4::zeros(); T + 1];
1702            let mut a_seq = Vec::with_capacity(T);
1703            let mut b_seq = Vec::with_capacity(T);
1704            x[0] = state.to_vector();
1705
1706            for t in 0..T {
1707                let (a, b, c) = get_linear_model_matrix(xbar[t][2], xbar[t][3], 0.0);
1708                x[t + 1] = a * x[t] + b * controls[t] + c;
1709                a_seq.push(a);
1710                b_seq.push(b);
1711            }
1712            let current_cost = compute_cost_without_angle_wrap(&x, xref, &controls);
1713
1714            let mut gradients = [Vector2::zeros(); T];
1715            let mut lambda = (qf * (x[T] - xref[T])) * 2.0;
1716
1717            for t in (0..T).rev() {
1718                let mut grad = (r * controls[t]) * 2.0 + b_seq[t].transpose() * lambda;
1719
1720                if t > 0 {
1721                    grad += (rd * (controls[t] - controls[t - 1])) * 2.0;
1722                }
1723                if t < T - 1 {
1724                    grad -= (rd * (controls[t + 1] - controls[t])) * 2.0;
1725                }
1726
1727                gradients[t] = grad;
1728
1729                let lx = if t == 0 {
1730                    Vector4::zeros()
1731                } else {
1732                    (q * (x[t] - xref[t])) * 2.0
1733                };
1734                lambda = lx + a_seq[t].transpose() * lambda;
1735            }
1736
1737            let gradient_norm = gradients
1738                .iter()
1739                .map(Vector2::norm_squared)
1740                .sum::<f64>()
1741                .sqrt();
1742
1743            if gradient_norm <= GRAD_TOL || (previous_cost - current_cost).abs() <= COST_TOL {
1744                trace.push(GradientTraceEntry {
1745                    iter,
1746                    cost: current_cost,
1747                    gradient_norm,
1748                    accepted_step: None,
1749                });
1750                break;
1751            }
1752
1753            previous_cost = current_cost;
1754            let current_controls = controls.clone();
1755            let mut step = initial_step;
1756            let mut accepted_step = None;
1757
1758            for _ in 0..line_search_iters {
1759                let mut candidate = current_controls.clone();
1760                for t in 0..T {
1761                    candidate[t] -= gradients[t] * step;
1762                }
1763                apply_control_constraints(&mut candidate);
1764
1765                let candidate_x = linearized_rollout_without_angle_wrap(state, xbar, &candidate);
1766                let candidate_cost =
1767                    compute_cost_without_angle_wrap(&candidate_x, xref, &candidate);
1768                if candidate_cost < current_cost {
1769                    controls = candidate;
1770                    accepted_step = Some(step);
1771                    break;
1772                }
1773
1774                step *= 0.5;
1775            }
1776
1777            trace.push(GradientTraceEntry {
1778                iter,
1779                cost: current_cost,
1780                gradient_norm,
1781                accepted_step,
1782            });
1783
1784            if accepted_step.is_none() {
1785                break;
1786            }
1787        }
1788
1789        (controls, trace)
1790    }
1791
1792    fn optimize_linearized_controls_without_angle_wrap_with_candidate_expansion(
1793        xref: &[Vector4<f64>],
1794        xbar: &[Vector4<f64>],
1795        state: &State,
1796        initial_controls: &[Vector2<f64>],
1797        max_iters: usize,
1798        step_candidates: &[f64],
1799    ) -> (Vec<Vector2<f64>>, Vec<GradientTraceEntry>) {
1800        let q = state_cost_weight();
1801        let qf = terminal_cost_weight();
1802        let r = control_cost_weight();
1803        let rd = control_rate_weight();
1804
1805        let mut controls = initial_controls.to_vec();
1806        controls.resize(T, Vector2::zeros());
1807        apply_control_constraints(&mut controls);
1808
1809        let mut previous_cost = f64::INFINITY;
1810        let mut trace = Vec::new();
1811
1812        for iter in 0..max_iters {
1813            let mut x = vec![Vector4::zeros(); T + 1];
1814            let mut a_seq = Vec::with_capacity(T);
1815            let mut b_seq = Vec::with_capacity(T);
1816            x[0] = state.to_vector();
1817
1818            for t in 0..T {
1819                let (a, b, c) = get_linear_model_matrix(xbar[t][2], xbar[t][3], 0.0);
1820                x[t + 1] = a * x[t] + b * controls[t] + c;
1821                a_seq.push(a);
1822                b_seq.push(b);
1823            }
1824            let current_cost = compute_cost_without_angle_wrap(&x, xref, &controls);
1825
1826            let mut gradients = [Vector2::zeros(); T];
1827            let mut lambda = (qf * (x[T] - xref[T])) * 2.0;
1828
1829            for t in (0..T).rev() {
1830                let mut grad = (r * controls[t]) * 2.0 + b_seq[t].transpose() * lambda;
1831
1832                if t > 0 {
1833                    grad += (rd * (controls[t] - controls[t - 1])) * 2.0;
1834                }
1835                if t < T - 1 {
1836                    grad -= (rd * (controls[t + 1] - controls[t])) * 2.0;
1837                }
1838
1839                gradients[t] = grad;
1840
1841                let lx = if t == 0 {
1842                    Vector4::zeros()
1843                } else {
1844                    (q * (x[t] - xref[t])) * 2.0
1845                };
1846                lambda = lx + a_seq[t].transpose() * lambda;
1847            }
1848
1849            let gradient_norm = gradients
1850                .iter()
1851                .map(Vector2::norm_squared)
1852                .sum::<f64>()
1853                .sqrt();
1854
1855            if gradient_norm <= GRAD_TOL || (previous_cost - current_cost).abs() <= COST_TOL {
1856                trace.push(GradientTraceEntry {
1857                    iter,
1858                    cost: current_cost,
1859                    gradient_norm,
1860                    accepted_step: None,
1861                });
1862                break;
1863            }
1864
1865            previous_cost = current_cost;
1866            let current_controls = controls.clone();
1867            let mut accepted_step = None;
1868            let mut best_candidate_controls = None;
1869            let mut best_candidate_cost = current_cost;
1870
1871            for &step in step_candidates {
1872                let mut candidate = current_controls.clone();
1873                for t in 0..T {
1874                    candidate[t] -= gradients[t] * step;
1875                }
1876                apply_control_constraints(&mut candidate);
1877
1878                let candidate_x = linearized_rollout_without_angle_wrap(state, xbar, &candidate);
1879                let candidate_cost =
1880                    compute_cost_without_angle_wrap(&candidate_x, xref, &candidate);
1881                if candidate_cost < best_candidate_cost {
1882                    best_candidate_cost = candidate_cost;
1883                    accepted_step = Some(step);
1884                    best_candidate_controls = Some(candidate);
1885                }
1886            }
1887
1888            trace.push(GradientTraceEntry {
1889                iter,
1890                cost: current_cost,
1891                gradient_norm,
1892                accepted_step,
1893            });
1894
1895            if let Some(candidate) = best_candidate_controls {
1896                controls = candidate;
1897            } else {
1898                break;
1899            }
1900        }
1901
1902        (controls, trace)
1903    }
1904
1905    fn optimize_linearized_controls_with_candidate_expansion(
1906        xref: &[Vector4<f64>],
1907        xbar: &[Vector4<f64>],
1908        state: &State,
1909        initial_controls: &[Vector2<f64>],
1910        max_iters: usize,
1911        step_candidates: &[f64],
1912    ) -> (Vec<Vector2<f64>>, Vec<GradientTraceEntry>) {
1913        let q = state_cost_weight();
1914        let qf = terminal_cost_weight();
1915        let r = control_cost_weight();
1916        let rd = control_rate_weight();
1917
1918        let mut controls = initial_controls.to_vec();
1919        controls.resize(T, Vector2::zeros());
1920        apply_control_constraints(&mut controls);
1921
1922        let mut previous_cost = f64::INFINITY;
1923        let mut trace = Vec::new();
1924
1925        for iter in 0..max_iters {
1926            let (x, a_seq, b_seq, _) = linearized_rollout(state, xbar, &controls);
1927            let current_cost = compute_cost(&x, xref, &controls);
1928
1929            let mut gradients = [Vector2::zeros(); T];
1930            let mut lambda = (qf * state_error(x[T], xref[T])) * 2.0;
1931
1932            for t in (0..T).rev() {
1933                let mut grad = (r * controls[t]) * 2.0 + b_seq[t].transpose() * lambda;
1934
1935                if t > 0 {
1936                    grad += (rd * (controls[t] - controls[t - 1])) * 2.0;
1937                }
1938                if t < T - 1 {
1939                    grad -= (rd * (controls[t + 1] - controls[t])) * 2.0;
1940                }
1941
1942                gradients[t] = grad;
1943
1944                let lx = if t == 0 {
1945                    Vector4::zeros()
1946                } else {
1947                    (q * state_error(x[t], xref[t])) * 2.0
1948                };
1949                lambda = lx + a_seq[t].transpose() * lambda;
1950            }
1951
1952            let gradient_norm = gradients
1953                .iter()
1954                .map(Vector2::norm_squared)
1955                .sum::<f64>()
1956                .sqrt();
1957
1958            if gradient_norm <= GRAD_TOL || (previous_cost - current_cost).abs() <= COST_TOL {
1959                trace.push(GradientTraceEntry {
1960                    iter,
1961                    cost: current_cost,
1962                    gradient_norm,
1963                    accepted_step: None,
1964                });
1965                break;
1966            }
1967
1968            previous_cost = current_cost;
1969            let current_controls = controls.clone();
1970            let mut accepted_step = None;
1971            let mut best_candidate_controls = None;
1972            let mut best_candidate_cost = current_cost;
1973
1974            for &step in step_candidates {
1975                let mut candidate = current_controls.clone();
1976                for t in 0..T {
1977                    candidate[t] -= gradients[t] * step;
1978                }
1979                apply_control_constraints(&mut candidate);
1980
1981                let (candidate_x, _, _, _) = linearized_rollout(state, xbar, &candidate);
1982                let candidate_cost = compute_cost(&candidate_x, xref, &candidate);
1983                if candidate_cost < best_candidate_cost {
1984                    best_candidate_cost = candidate_cost;
1985                    accepted_step = Some(step);
1986                    best_candidate_controls = Some(candidate);
1987                }
1988            }
1989
1990            trace.push(GradientTraceEntry {
1991                iter,
1992                cost: current_cost,
1993                gradient_norm,
1994                accepted_step,
1995            });
1996
1997            if let Some(candidate) = best_candidate_controls {
1998                controls = candidate;
1999            } else {
2000                break;
2001            }
2002        }
2003
2004        (controls, trace)
2005    }
2006
2007    fn expanded_backtracking_step_candidates() -> Vec<f64> {
2008        let mut steps = vec![1.0, 0.5];
2009        let mut step = LINE_SEARCH_INITIAL_STEP;
2010        for _ in 0..LINE_SEARCH_ITERS {
2011            steps.push(step);
2012            step *= 0.5;
2013        }
2014        steps
2015    }
2016
2017    fn optimize_linearized_controls_with_full_candidate_expansion(
2018        xref: &[Vector4<f64>],
2019        xbar: &[Vector4<f64>],
2020        state: &State,
2021        initial_controls: &[Vector2<f64>],
2022    ) -> Vec<Vector2<f64>> {
2023        optimize_linearized_controls_with_candidate_expansion(
2024            xref,
2025            xbar,
2026            state,
2027            initial_controls,
2028            QP_MAX_ITERS,
2029            &expanded_backtracking_step_candidates(),
2030        )
2031        .0
2032    }
2033
2034    fn iterative_linear_mpc_control_with_test_optimizer<F>(
2035        xref: &[Vector4<f64>],
2036        state: &State,
2037        warm_start: &[Vector2<f64>],
2038        optimize: F,
2039    ) -> MpcResult
2040    where
2041        F: Fn(&[Vector4<f64>], &[Vector4<f64>], &State, &[Vector2<f64>]) -> Vec<Vector2<f64>>,
2042    {
2043        let mut controls = warm_start.to_vec();
2044        controls.resize(T, Vector2::zeros());
2045        apply_control_constraints(&mut controls);
2046
2047        let mut predicted = predict_motion(*state, &controls);
2048        for _ in 0..MAX_ITER {
2049            let previous_controls = controls.clone();
2050            controls = optimize(xref, &predicted, state, &controls);
2051            predicted = predict_motion(*state, &controls);
2052
2053            let du = controls
2054                .iter()
2055                .zip(previous_controls.iter())
2056                .map(|(current, previous)| {
2057                    (current[0] - previous[0]).abs() + (current[1] - previous[1]).abs()
2058                })
2059                .sum::<f64>();
2060
2061            if du <= DU_TH {
2062                break;
2063            }
2064        }
2065
2066        MpcResult {
2067            controls,
2068            predicted,
2069        }
2070    }
2071
2072    fn trace_projection_effect_against_python_reference(
2073        xref: &[Vector4<f64>],
2074        xbar: &[Vector4<f64>],
2075        state: &State,
2076        initial_controls: &[Vector2<f64>],
2077        python_controls: &[Vector2<f64>],
2078        max_iters: usize,
2079        line_search_iters: usize,
2080    ) -> Vec<ProjectionTraceEntry> {
2081        let q = state_cost_weight();
2082        let qf = terminal_cost_weight();
2083        let r = control_cost_weight();
2084        let rd = control_rate_weight();
2085
2086        let mut controls = initial_controls.to_vec();
2087        controls.resize(T, Vector2::zeros());
2088        apply_control_constraints(&mut controls);
2089
2090        let mut previous_cost = f64::INFINITY;
2091        let mut trace = Vec::new();
2092
2093        for iter in 0..max_iters {
2094            let mut x = vec![Vector4::zeros(); T + 1];
2095            let mut a_seq = Vec::with_capacity(T);
2096            let mut b_seq = Vec::with_capacity(T);
2097            x[0] = state.to_vector();
2098
2099            for t in 0..T {
2100                let (a, b, c) = get_linear_model_matrix(xbar[t][2], xbar[t][3], 0.0);
2101                x[t + 1] = a * x[t] + b * controls[t] + c;
2102                a_seq.push(a);
2103                b_seq.push(b);
2104            }
2105            let current_cost = compute_cost_without_angle_wrap(&x, xref, &controls);
2106
2107            let mut gradients = [Vector2::zeros(); T];
2108            let mut lambda = (qf * (x[T] - xref[T])) * 2.0;
2109
2110            for t in (0..T).rev() {
2111                let mut grad = (r * controls[t]) * 2.0 + b_seq[t].transpose() * lambda;
2112
2113                if t > 0 {
2114                    grad += (rd * (controls[t] - controls[t - 1])) * 2.0;
2115                }
2116                if t < T - 1 {
2117                    grad -= (rd * (controls[t + 1] - controls[t])) * 2.0;
2118                }
2119
2120                gradients[t] = grad;
2121
2122                let lx = if t == 0 {
2123                    Vector4::zeros()
2124                } else {
2125                    (q * (x[t] - xref[t])) * 2.0
2126                };
2127                lambda = lx + a_seq[t].transpose() * lambda;
2128            }
2129
2130            let gradient_norm = gradients
2131                .iter()
2132                .map(Vector2::norm_squared)
2133                .sum::<f64>()
2134                .sqrt();
2135
2136            if gradient_norm <= GRAD_TOL || (previous_cost - current_cost).abs() <= COST_TOL {
2137                break;
2138            }
2139
2140            previous_cost = current_cost;
2141            let current_controls = controls.clone();
2142            let mut step = 0.25;
2143            let mut candidates = Vec::new();
2144            let mut improved = false;
2145
2146            for _ in 0..line_search_iters {
2147                let mut raw_candidate = current_controls.clone();
2148                for t in 0..T {
2149                    raw_candidate[t] -= gradients[t] * step;
2150                }
2151
2152                let raw_x = linearized_rollout_without_angle_wrap(state, xbar, &raw_candidate);
2153                let raw_cost = compute_cost_without_angle_wrap(&raw_x, xref, &raw_candidate);
2154
2155                let mut projected_candidate = raw_candidate.clone();
2156                apply_control_constraints(&mut projected_candidate);
2157                let projected_x =
2158                    linearized_rollout_without_angle_wrap(state, xbar, &projected_candidate);
2159                let projected_cost =
2160                    compute_cost_without_angle_wrap(&projected_x, xref, &projected_candidate);
2161
2162                candidates.push(LineSearchProjectionEntry {
2163                    step,
2164                    raw_cost,
2165                    projected_cost,
2166                    raw_to_projected: control_sequence_max_diff(
2167                        &raw_candidate,
2168                        &projected_candidate,
2169                    ),
2170                    raw_to_python: control_sequence_max_diff(&raw_candidate, python_controls),
2171                    projected_to_python: control_sequence_max_diff(
2172                        &projected_candidate,
2173                        python_controls,
2174                    ),
2175                });
2176
2177                if projected_cost < current_cost {
2178                    controls = projected_candidate;
2179                    improved = true;
2180                    break;
2181                }
2182
2183                step *= 0.5;
2184            }
2185
2186            trace.push(ProjectionTraceEntry {
2187                iter,
2188                current_cost,
2189                candidates,
2190            });
2191
2192            if !improved {
2193                break;
2194            }
2195        }
2196
2197        trace
2198    }
2199
2200    fn first_iteration_projection_candidates(
2201        xref: &[Vector4<f64>],
2202        xbar: &[Vector4<f64>],
2203        state: &State,
2204        initial_controls: &[Vector2<f64>],
2205        python_controls: &[Vector2<f64>],
2206        step_sizes: &[f64],
2207    ) -> (f64, Vec<LineSearchProjectionEntry>) {
2208        let q = state_cost_weight();
2209        let qf = terminal_cost_weight();
2210        let r = control_cost_weight();
2211        let rd = control_rate_weight();
2212
2213        let mut controls = initial_controls.to_vec();
2214        controls.resize(T, Vector2::zeros());
2215        apply_control_constraints(&mut controls);
2216
2217        let mut x = vec![Vector4::zeros(); T + 1];
2218        let mut a_seq = Vec::with_capacity(T);
2219        let mut b_seq = Vec::with_capacity(T);
2220        x[0] = state.to_vector();
2221
2222        for t in 0..T {
2223            let (a, b, c) = get_linear_model_matrix(xbar[t][2], xbar[t][3], 0.0);
2224            x[t + 1] = a * x[t] + b * controls[t] + c;
2225            a_seq.push(a);
2226            b_seq.push(b);
2227        }
2228
2229        let current_cost = compute_cost_without_angle_wrap(&x, xref, &controls);
2230        let mut gradients = [Vector2::zeros(); T];
2231        let mut lambda = (qf * (x[T] - xref[T])) * 2.0;
2232
2233        for t in (0..T).rev() {
2234            let mut grad = (r * controls[t]) * 2.0 + b_seq[t].transpose() * lambda;
2235
2236            if t > 0 {
2237                grad += (rd * (controls[t] - controls[t - 1])) * 2.0;
2238            }
2239            if t < T - 1 {
2240                grad -= (rd * (controls[t + 1] - controls[t])) * 2.0;
2241            }
2242
2243            gradients[t] = grad;
2244
2245            let lx = if t == 0 {
2246                Vector4::zeros()
2247            } else {
2248                (q * (x[t] - xref[t])) * 2.0
2249            };
2250            lambda = lx + a_seq[t].transpose() * lambda;
2251        }
2252
2253        let mut candidates = Vec::new();
2254        for &step in step_sizes {
2255            let mut raw_candidate = controls.clone();
2256            for t in 0..T {
2257                raw_candidate[t] -= gradients[t] * step;
2258            }
2259
2260            let raw_x = linearized_rollout_without_angle_wrap(state, xbar, &raw_candidate);
2261            let raw_cost = compute_cost_without_angle_wrap(&raw_x, xref, &raw_candidate);
2262
2263            let mut projected_candidate = raw_candidate.clone();
2264            apply_control_constraints(&mut projected_candidate);
2265            let projected_x =
2266                linearized_rollout_without_angle_wrap(state, xbar, &projected_candidate);
2267            let projected_cost =
2268                compute_cost_without_angle_wrap(&projected_x, xref, &projected_candidate);
2269
2270            candidates.push(LineSearchProjectionEntry {
2271                step,
2272                raw_cost,
2273                projected_cost,
2274                raw_to_projected: control_sequence_max_diff(&raw_candidate, &projected_candidate),
2275                raw_to_python: control_sequence_max_diff(&raw_candidate, python_controls),
2276                projected_to_python: control_sequence_max_diff(
2277                    &projected_candidate,
2278                    python_controls,
2279                ),
2280            });
2281        }
2282
2283        (current_cost, candidates)
2284    }
2285
2286    fn state_from_vector4(state: &Vector4<f64>) -> State {
2287        State::new(state[0], state[1], state[2], state[3])
2288    }
2289
2290    fn inject_state_components(
2291        base: &Vector4<f64>,
2292        injected: &Vector4<f64>,
2293        component_indices: &[usize],
2294    ) -> State {
2295        let mut hybrid = *base;
2296        for index in component_indices {
2297            hybrid[*index] = injected[*index];
2298        }
2299        state_from_vector4(&hybrid)
2300    }
2301
2302    fn measure_switch_back_state_injection(indices: &[usize]) -> StateInjectionMetrics {
2303        let default_trace = collect_switch_back_detailed_trace_with_test_optimizer(
2304            330,
2305            optimize_linearized_controls_original,
2306        );
2307        let expanded_trace = collect_switch_back_detailed_trace_with_test_optimizer(
2308            330,
2309            optimize_linearized_controls_with_full_candidate_expansion,
2310        );
2311        let default_warm_start = collect_switch_back_warm_start_trace_with_test_optimizer(
2312            330,
2313            optimize_linearized_controls_original,
2314        );
2315        let expanded_warm_start = collect_switch_back_warm_start_trace_with_test_optimizer(
2316            330,
2317            optimize_linearized_controls_with_full_candidate_expansion,
2318        );
2319
2320        let mut summary = StateInjectionMetrics::default();
2321        for metrics in measure_switch_back_state_injection_by_step_with_context(
2322            indices,
2323            &[325usize, 326, 327, 328, 329],
2324            &default_trace,
2325            &expanded_trace,
2326            &default_warm_start,
2327            &expanded_warm_start,
2328        )
2329        .into_values()
2330        {
2331            summary.default_same_control0 += metrics.default_same_control0;
2332            summary.default_opposite_control0 += metrics.default_opposite_control0;
2333            summary.default_same_pred_y += metrics.default_same_pred_y;
2334            summary.default_opposite_pred_y += metrics.default_opposite_pred_y;
2335            summary.default_same_pred_v += metrics.default_same_pred_v;
2336            summary.default_opposite_pred_v += metrics.default_opposite_pred_v;
2337            summary.expanded_same_control0 += metrics.expanded_same_control0;
2338            summary.expanded_opposite_control0 += metrics.expanded_opposite_control0;
2339            summary.expanded_same_pred_y += metrics.expanded_same_pred_y;
2340            summary.expanded_opposite_pred_y += metrics.expanded_opposite_pred_y;
2341            summary.expanded_same_pred_v += metrics.expanded_same_pred_v;
2342            summary.expanded_opposite_pred_v += metrics.expanded_opposite_pred_v;
2343        }
2344
2345        summary
2346    }
2347
2348    fn measure_switch_back_state_injection_by_step_with_context(
2349        indices: &[usize],
2350        steps: &[usize],
2351        default_trace: &[SwitchBackDetailedRow],
2352        expanded_trace: &[SwitchBackDetailedRow],
2353        default_warm_start: &SwitchBackWarmStartTrace,
2354        expanded_warm_start: &SwitchBackWarmStartTrace,
2355    ) -> BTreeMap<usize, StateInjectionMetrics> {
2356        let mut by_step = BTreeMap::new();
2357
2358        for step in steps {
2359            let mut metrics = StateInjectionMetrics::default();
2360            let default_row = detailed_row_at_step(default_trace, *step);
2361            let expanded_row = detailed_row_at_step(expanded_trace, *step);
2362            let default_injected = iterative_linear_mpc_control_with_test_optimizer(
2363                &default_row.xref,
2364                &inject_state_components(&default_row.state, &expanded_row.state, indices),
2365                default_warm_start.get(step).unwrap(),
2366                optimize_linearized_controls_original,
2367            );
2368            let expanded_injected = iterative_linear_mpc_control_with_test_optimizer(
2369                &expanded_row.xref,
2370                &inject_state_components(&expanded_row.state, &default_row.state, indices),
2371                expanded_warm_start.get(step).unwrap(),
2372                optimize_linearized_controls_with_full_candidate_expansion,
2373            );
2374
2375            let default_baseline_controls = default_row
2376                .controls
2377                .iter()
2378                .map(|vec| [vec[0], vec[1]])
2379                .collect::<Vec<_>>();
2380            let expanded_baseline_controls = expanded_row
2381                .controls
2382                .iter()
2383                .map(|vec| [vec[0], vec[1]])
2384                .collect::<Vec<_>>();
2385            let default_baseline_predicted = default_row
2386                .predicted
2387                .iter()
2388                .map(|vec| [vec[0], vec[1], vec[2], vec[3]])
2389                .collect::<Vec<_>>();
2390            let expanded_baseline_predicted = expanded_row
2391                .predicted
2392                .iter()
2393                .map(|vec| [vec[0], vec[1], vec[2], vec[3]])
2394                .collect::<Vec<_>>();
2395
2396            let default_to_default_control = summarize_vec2_sequence_diff(
2397                &default_injected.controls,
2398                &default_baseline_controls,
2399            );
2400            let default_to_expanded_control = summarize_vec2_sequence_diff(
2401                &default_injected.controls,
2402                &expanded_baseline_controls,
2403            );
2404            let default_to_default_predicted = summarize_vec4_sequence_diff(
2405                &default_injected.predicted,
2406                &default_baseline_predicted,
2407            );
2408            let default_to_expanded_predicted = summarize_vec4_sequence_diff(
2409                &default_injected.predicted,
2410                &expanded_baseline_predicted,
2411            );
2412
2413            let expanded_to_expanded_control = summarize_vec2_sequence_diff(
2414                &expanded_injected.controls,
2415                &expanded_baseline_controls,
2416            );
2417            let expanded_to_default_control = summarize_vec2_sequence_diff(
2418                &expanded_injected.controls,
2419                &default_baseline_controls,
2420            );
2421            let expanded_to_expanded_predicted = summarize_vec4_sequence_diff(
2422                &expanded_injected.predicted,
2423                &expanded_baseline_predicted,
2424            );
2425            let expanded_to_default_predicted = summarize_vec4_sequence_diff(
2426                &expanded_injected.predicted,
2427                &default_baseline_predicted,
2428            );
2429
2430            metrics.default_same_control0 = default_to_default_control[0];
2431            metrics.default_opposite_control0 = default_to_expanded_control[0];
2432            metrics.default_same_pred_y = default_to_default_predicted[1];
2433            metrics.default_opposite_pred_y = default_to_expanded_predicted[1];
2434            metrics.default_same_pred_v = default_to_default_predicted[2];
2435            metrics.default_opposite_pred_v = default_to_expanded_predicted[2];
2436
2437            metrics.expanded_same_control0 = expanded_to_expanded_control[0];
2438            metrics.expanded_opposite_control0 = expanded_to_default_control[0];
2439            metrics.expanded_same_pred_y = expanded_to_expanded_predicted[1];
2440            metrics.expanded_opposite_pred_y = expanded_to_default_predicted[1];
2441            metrics.expanded_same_pred_v = expanded_to_expanded_predicted[2];
2442            metrics.expanded_opposite_pred_v = expanded_to_default_predicted[2];
2443
2444            by_step.insert(*step, metrics);
2445        }
2446
2447        by_step
2448    }
2449
2450    fn measure_switch_back_state_injection_by_step(
2451        indices: &[usize],
2452        steps: &[usize],
2453    ) -> BTreeMap<usize, StateInjectionMetrics> {
2454        let default_trace = collect_switch_back_detailed_trace_with_test_optimizer(
2455            330,
2456            optimize_linearized_controls_original,
2457        );
2458        let expanded_trace = collect_switch_back_detailed_trace_with_test_optimizer(
2459            330,
2460            optimize_linearized_controls_with_full_candidate_expansion,
2461        );
2462        let default_warm_start = collect_switch_back_warm_start_trace_with_test_optimizer(
2463            330,
2464            optimize_linearized_controls_original,
2465        );
2466        let expanded_warm_start = collect_switch_back_warm_start_trace_with_test_optimizer(
2467            330,
2468            optimize_linearized_controls_with_full_candidate_expansion,
2469        );
2470
2471        measure_switch_back_state_injection_by_step_with_context(
2472            indices,
2473            steps,
2474            &default_trace,
2475            &expanded_trace,
2476            &default_warm_start,
2477            &expanded_warm_start,
2478        )
2479    }
2480
2481    fn state_injection_metrics_at_step(
2482        metrics: &BTreeMap<usize, StateInjectionMetrics>,
2483        step: usize,
2484    ) -> &StateInjectionMetrics {
2485        metrics
2486            .get(&step)
2487            .unwrap_or_else(|| panic!("missing state-injection metrics for step {step}"))
2488    }
2489
2490    fn advance_switch_back_context(steps: usize) -> SwitchBackContext {
2491        let (cx, cy, cyaw) = generate_switch_back_course();
2492        let speed_profile = calc_speed_profile(&cx, &cy, &cyaw, TARGET_SPEED);
2493
2494        let mut state = State::new(cx[0], cy[0], 0.0, cyaw[0]);
2495        if state.yaw - cyaw[0] >= PI {
2496            state.yaw -= 2.0 * PI;
2497        } else if state.yaw - cyaw[0] <= -PI {
2498            state.yaw += 2.0 * PI;
2499        }
2500
2501        let mut target_index = 0;
2502        let mut warm_start = vec![Vector2::zeros(); T];
2503
2504        for _ in 0..steps {
2505            let (xref, new_index) = calc_ref_trajectory(
2506                &state,
2507                &cx,
2508                &cy,
2509                &cyaw,
2510                &speed_profile,
2511                UPSTREAM_SWITCH_BACK_TICK,
2512                target_index,
2513            );
2514            target_index = new_index;
2515
2516            let mpc_result = iterative_linear_mpc_control(&xref, &state, &warm_start);
2517            state.update(mpc_result.controls[0][0], mpc_result.controls[0][1]);
2518
2519            warm_start = next_warm_start(&mpc_result.controls);
2520        }
2521
2522        SwitchBackContext {
2523            cx,
2524            cy,
2525            cyaw,
2526            speed_profile,
2527            state,
2528            target_index,
2529            warm_start,
2530        }
2531    }
2532
2533    fn collect_switch_back_prefix_trace(steps: usize) -> Vec<SwitchBackTraceRow> {
2534        let (cx, cy, cyaw) = generate_switch_back_course();
2535        let speed_profile = calc_speed_profile(&cx, &cy, &cyaw, TARGET_SPEED);
2536
2537        let mut state = State::new(cx[0], cy[0], 0.0, cyaw[0]);
2538        if state.yaw - cyaw[0] >= PI {
2539            state.yaw -= 2.0 * PI;
2540        } else if state.yaw - cyaw[0] <= -PI {
2541            state.yaw += 2.0 * PI;
2542        }
2543
2544        let mut target_index = 0;
2545        let mut warm_start = vec![Vector2::zeros(); T];
2546        let mut trace = Vec::with_capacity(steps);
2547
2548        for step in 0..steps {
2549            let (xref, new_index) = calc_ref_trajectory(
2550                &state,
2551                &cx,
2552                &cy,
2553                &cyaw,
2554                &speed_profile,
2555                UPSTREAM_SWITCH_BACK_TICK,
2556                target_index,
2557            );
2558            target_index = new_index;
2559
2560            let result = iterative_linear_mpc_control(&xref, &state, &warm_start);
2561            trace.push(SwitchBackTraceRow {
2562                step,
2563                target_index,
2564                state: state.to_vector(),
2565                xref_head: xref[0],
2566                xref_tail: *xref.last().unwrap(),
2567                control: result.controls[0],
2568                predicted_tail: *result.predicted.last().unwrap(),
2569            });
2570
2571            state.update(result.controls[0][0], result.controls[0][1]);
2572            warm_start = next_warm_start(&result.controls);
2573        }
2574
2575        trace
2576    }
2577
2578    fn collect_switch_back_detailed_trace_with_test_optimizer<F>(
2579        steps: usize,
2580        optimize: F,
2581    ) -> Vec<SwitchBackDetailedRow>
2582    where
2583        F: Fn(&[Vector4<f64>], &[Vector4<f64>], &State, &[Vector2<f64>]) -> Vec<Vector2<f64>>
2584            + Copy,
2585    {
2586        let (cx, cy, cyaw) = generate_switch_back_course();
2587        let speed_profile = calc_speed_profile(&cx, &cy, &cyaw, TARGET_SPEED);
2588
2589        let mut state = State::new(cx[0], cy[0], 0.0, cyaw[0]);
2590        if state.yaw - cyaw[0] >= PI {
2591            state.yaw -= 2.0 * PI;
2592        } else if state.yaw - cyaw[0] <= -PI {
2593            state.yaw += 2.0 * PI;
2594        }
2595
2596        let mut target_index = 0;
2597        let mut warm_start = vec![Vector2::zeros(); T];
2598        let mut trace = Vec::with_capacity(steps);
2599
2600        for step in 0..steps {
2601            let (xref, new_index) = calc_ref_trajectory(
2602                &state,
2603                &cx,
2604                &cy,
2605                &cyaw,
2606                &speed_profile,
2607                UPSTREAM_SWITCH_BACK_TICK,
2608                target_index,
2609            );
2610            target_index = new_index;
2611
2612            let result = iterative_linear_mpc_control_with_test_optimizer(
2613                &xref,
2614                &state,
2615                &warm_start,
2616                optimize,
2617            );
2618            trace.push(SwitchBackDetailedRow {
2619                step,
2620                target_index,
2621                state: state.to_vector(),
2622                xref: xref.clone(),
2623                controls: result.controls.clone(),
2624                predicted: result.predicted.clone(),
2625            });
2626
2627            state.update(result.controls[0][0], result.controls[0][1]);
2628            warm_start = next_warm_start(&result.controls);
2629        }
2630
2631        trace
2632    }
2633
2634    fn collect_switch_back_planning_snapshots_with_test_optimizer<F>(
2635        steps: usize,
2636        checkpoints: &[usize],
2637        optimize: F,
2638    ) -> Vec<SwitchBackPlanningSnapshot>
2639    where
2640        F: Fn(&[Vector4<f64>], &[Vector4<f64>], &State, &[Vector2<f64>]) -> Vec<Vector2<f64>>
2641            + Copy,
2642    {
2643        let (cx, cy, cyaw) = generate_switch_back_course();
2644        let speed_profile = calc_speed_profile(&cx, &cy, &cyaw, TARGET_SPEED);
2645
2646        let mut state = State::new(cx[0], cy[0], 0.0, cyaw[0]);
2647        if state.yaw - cyaw[0] >= PI {
2648            state.yaw -= 2.0 * PI;
2649        } else if state.yaw - cyaw[0] <= -PI {
2650            state.yaw += 2.0 * PI;
2651        }
2652
2653        let mut target_index = 0;
2654        let mut warm_start = vec![Vector2::zeros(); T];
2655        let mut trace = Vec::new();
2656
2657        for step in 0..steps {
2658            let (xref, new_index) = calc_ref_trajectory(
2659                &state,
2660                &cx,
2661                &cy,
2662                &cyaw,
2663                &speed_profile,
2664                UPSTREAM_SWITCH_BACK_TICK,
2665                target_index,
2666            );
2667            target_index = new_index;
2668
2669            if checkpoints.contains(&step) {
2670                trace.push(SwitchBackPlanningSnapshot {
2671                    step,
2672                    target_index,
2673                    state: state.to_vector(),
2674                    xref_head: xref[0],
2675                    warm_start_head: warm_start.first().copied().unwrap_or_else(Vector2::zeros),
2676                });
2677            }
2678
2679            let result = iterative_linear_mpc_control_with_test_optimizer(
2680                &xref,
2681                &state,
2682                &warm_start,
2683                optimize,
2684            );
2685            state.update(result.controls[0][0], result.controls[0][1]);
2686            warm_start = next_warm_start(&result.controls);
2687        }
2688
2689        trace
2690    }
2691
2692    fn collect_switch_back_warm_start_trace_with_test_optimizer<F>(
2693        steps: usize,
2694        optimize: F,
2695    ) -> SwitchBackWarmStartTrace
2696    where
2697        F: Fn(&[Vector4<f64>], &[Vector4<f64>], &State, &[Vector2<f64>]) -> Vec<Vector2<f64>>
2698            + Copy,
2699    {
2700        let (cx, cy, cyaw) = generate_switch_back_course();
2701        let speed_profile = calc_speed_profile(&cx, &cy, &cyaw, TARGET_SPEED);
2702
2703        let mut state = State::new(cx[0], cy[0], 0.0, cyaw[0]);
2704        if state.yaw - cyaw[0] >= PI {
2705            state.yaw -= 2.0 * PI;
2706        } else if state.yaw - cyaw[0] <= -PI {
2707            state.yaw += 2.0 * PI;
2708        }
2709
2710        let mut target_index = 0;
2711        let mut warm_start = vec![Vector2::zeros(); T];
2712        let mut trace = SwitchBackWarmStartTrace::new();
2713
2714        for step in 0..steps {
2715            let (xref, new_index) = calc_ref_trajectory(
2716                &state,
2717                &cx,
2718                &cy,
2719                &cyaw,
2720                &speed_profile,
2721                UPSTREAM_SWITCH_BACK_TICK,
2722                target_index,
2723            );
2724            target_index = new_index;
2725            trace.insert(step, warm_start.clone());
2726
2727            let result = iterative_linear_mpc_control_with_test_optimizer(
2728                &xref,
2729                &state,
2730                &warm_start,
2731                optimize,
2732            );
2733            state.update(result.controls[0][0], result.controls[0][1]);
2734            warm_start = next_warm_start(&result.controls);
2735        }
2736
2737        trace
2738    }
2739
2740    fn collect_switch_back_detailed_trace(steps: usize) -> Vec<SwitchBackDetailedRow> {
2741        collect_switch_back_detailed_trace_with_test_optimizer(steps, optimize_linearized_controls)
2742    }
2743
2744    fn run_switch_back_simulation_with_test_optimizer<F>(
2745        max_sim_steps: usize,
2746        optimize: F,
2747    ) -> SimulationResult
2748    where
2749        F: Fn(&[Vector4<f64>], &[Vector4<f64>], &State, &[Vector2<f64>]) -> Vec<Vector2<f64>>
2750            + Copy,
2751    {
2752        let (cx, cy, cyaw) = generate_switch_back_course();
2753        let speed_profile = calc_speed_profile(&cx, &cy, &cyaw, TARGET_SPEED);
2754
2755        let mut state = State::new(cx[0], cy[0], 0.0, cyaw[0]);
2756        if state.yaw - cyaw[0] >= PI {
2757            state.yaw -= 2.0 * PI;
2758        } else if state.yaw - cyaw[0] <= -PI {
2759            state.yaw += 2.0 * PI;
2760        }
2761
2762        let mut target_index = 0;
2763        let final_index = cx.len() - 1;
2764        let goal = (cx[final_index], cy[final_index]);
2765        let mut warm_start = vec![Vector2::zeros(); T];
2766
2767        let mut hist_x = vec![state.x];
2768        let mut hist_y = vec![state.y];
2769        let mut predicted = vec![state.to_vector()];
2770        let mut reached_goal = false;
2771
2772        for _ in 0..max_sim_steps {
2773            let (xref, new_index) = calc_ref_trajectory(
2774                &state,
2775                &cx,
2776                &cy,
2777                &cyaw,
2778                &speed_profile,
2779                UPSTREAM_SWITCH_BACK_TICK,
2780                target_index,
2781            );
2782            target_index = new_index;
2783
2784            let mpc_result = iterative_linear_mpc_control_with_test_optimizer(
2785                &xref,
2786                &state,
2787                &warm_start,
2788                optimize,
2789            );
2790            let accel = mpc_result.controls[0][0];
2791            let steer = mpc_result.controls[0][1];
2792
2793            state.update(accel, steer);
2794            hist_x.push(state.x);
2795            hist_y.push(state.y);
2796            predicted = mpc_result.predicted;
2797
2798            warm_start = next_warm_start(&mpc_result.controls);
2799
2800            if check_goal(&state, goal, target_index, final_index) {
2801                reached_goal = true;
2802                break;
2803            }
2804        }
2805
2806        SimulationResult {
2807            cx,
2808            cy,
2809            hist_x,
2810            hist_y,
2811            predicted_x: predicted.iter().map(|p| p[0]).collect(),
2812            predicted_y: predicted.iter().map(|p| p[1]).collect(),
2813            goal,
2814            reached_goal,
2815            target_index,
2816            final_index,
2817            final_state: state,
2818        }
2819    }
2820
2821    fn collect_switch_back_closed_loop_checkpoints_with_test_optimizer<F>(
2822        max_sim_steps: usize,
2823        checkpoints: &[usize],
2824        optimize: F,
2825    ) -> Vec<SwitchBackClosedLoopCheckpoint>
2826    where
2827        F: Fn(&[Vector4<f64>], &[Vector4<f64>], &State, &[Vector2<f64>]) -> Vec<Vector2<f64>>
2828            + Copy,
2829    {
2830        let (cx, cy, cyaw) = generate_switch_back_course();
2831        let speed_profile = calc_speed_profile(&cx, &cy, &cyaw, TARGET_SPEED);
2832
2833        let mut state = State::new(cx[0], cy[0], 0.0, cyaw[0]);
2834        if state.yaw - cyaw[0] >= PI {
2835            state.yaw -= 2.0 * PI;
2836        } else if state.yaw - cyaw[0] <= -PI {
2837            state.yaw += 2.0 * PI;
2838        }
2839
2840        let mut target_index = 0;
2841        let final_index = cx.len() - 1;
2842        let goal = (cx[final_index], cy[final_index]);
2843        let mut warm_start = vec![Vector2::zeros(); T];
2844        let mut recorded = Vec::new();
2845
2846        for step in 0..max_sim_steps {
2847            let (xref, new_index) = calc_ref_trajectory(
2848                &state,
2849                &cx,
2850                &cy,
2851                &cyaw,
2852                &speed_profile,
2853                UPSTREAM_SWITCH_BACK_TICK,
2854                target_index,
2855            );
2856            target_index = new_index;
2857
2858            let mpc_result = iterative_linear_mpc_control_with_test_optimizer(
2859                &xref,
2860                &state,
2861                &warm_start,
2862                optimize,
2863            );
2864            let accel = mpc_result.controls[0][0];
2865            let steer = mpc_result.controls[0][1];
2866
2867            state.update(accel, steer);
2868            warm_start = next_warm_start(&mpc_result.controls);
2869
2870            if checkpoints.contains(&(step + 1)) {
2871                let goal_distance =
2872                    ((state.x - goal.0).powi(2) + (state.y - goal.1).powi(2)).sqrt();
2873                recorded.push(SwitchBackClosedLoopCheckpoint {
2874                    step: step + 1,
2875                    target_index,
2876                    goal_distance,
2877                    state: state.to_vector(),
2878                });
2879            }
2880
2881            if check_goal(&state, goal, target_index, final_index) {
2882                break;
2883            }
2884        }
2885
2886        recorded
2887    }
2888
2889    fn collect_switch_back_goal_condition_snapshots_with_test_optimizer<F>(
2890        max_sim_steps: usize,
2891        checkpoints: &[usize],
2892        optimize: F,
2893    ) -> Vec<SwitchBackGoalConditionSnapshot>
2894    where
2895        F: Fn(&[Vector4<f64>], &[Vector4<f64>], &State, &[Vector2<f64>]) -> Vec<Vector2<f64>>
2896            + Copy,
2897    {
2898        let (cx, cy, cyaw) = generate_switch_back_course();
2899        let speed_profile = calc_speed_profile(&cx, &cy, &cyaw, TARGET_SPEED);
2900
2901        let mut state = State::new(cx[0], cy[0], 0.0, cyaw[0]);
2902        if state.yaw - cyaw[0] >= PI {
2903            state.yaw -= 2.0 * PI;
2904        } else if state.yaw - cyaw[0] <= -PI {
2905            state.yaw += 2.0 * PI;
2906        }
2907
2908        let mut target_index = 0;
2909        let final_index = cx.len() - 1;
2910        let goal = (cx[final_index], cy[final_index]);
2911        let mut warm_start = vec![Vector2::zeros(); T];
2912        let mut recorded = Vec::new();
2913
2914        for step in 0..max_sim_steps {
2915            let (xref, new_index) = calc_ref_trajectory(
2916                &state,
2917                &cx,
2918                &cy,
2919                &cyaw,
2920                &speed_profile,
2921                UPSTREAM_SWITCH_BACK_TICK,
2922                target_index,
2923            );
2924            target_index = new_index;
2925
2926            let mpc_result = iterative_linear_mpc_control_with_test_optimizer(
2927                &xref,
2928                &state,
2929                &warm_start,
2930                optimize,
2931            );
2932            let accel = mpc_result.controls[0][0];
2933            let steer = mpc_result.controls[0][1];
2934
2935            state.update(accel, steer);
2936            warm_start = next_warm_start(&mpc_result.controls);
2937
2938            let dx = state.x - goal.0;
2939            let dy = state.y - goal.1;
2940            let goal_distance = (dx * dx + dy * dy).sqrt();
2941            let heading_error = angle_diff(state.yaw, cyaw[target_index]);
2942            let motion_curvature = steer.tan() / WB;
2943            let near_goal = goal_distance <= GOAL_DIS;
2944            let near_end = final_index.abs_diff(target_index) < 5;
2945            let stopped = state.v.abs() <= STOP_SPEED;
2946            let reached_goal = near_goal && near_end && stopped;
2947
2948            if checkpoints.contains(&(step + 1)) {
2949                recorded.push(SwitchBackGoalConditionSnapshot {
2950                    step: step + 1,
2951                    target_index,
2952                    goal_distance,
2953                    speed: state.v,
2954                    speed_abs: state.v.abs(),
2955                    heading_error,
2956                    motion_curvature,
2957                    near_goal,
2958                    near_end,
2959                    stopped,
2960                    reached_goal,
2961                });
2962            }
2963
2964            if reached_goal {
2965                break;
2966            }
2967        }
2968
2969        recorded
2970    }
2971
2972    fn collect_switch_back_goal_control_snapshots_with_test_optimizer<F>(
2973        max_sim_steps: usize,
2974        checkpoints: &[usize],
2975        optimize: F,
2976    ) -> Vec<SwitchBackGoalControlSnapshot>
2977    where
2978        F: Fn(&[Vector4<f64>], &[Vector4<f64>], &State, &[Vector2<f64>]) -> Vec<Vector2<f64>>
2979            + Copy,
2980    {
2981        let (cx, cy, cyaw) = generate_switch_back_course();
2982        let speed_profile = calc_speed_profile(&cx, &cy, &cyaw, TARGET_SPEED);
2983
2984        let mut state = State::new(cx[0], cy[0], 0.0, cyaw[0]);
2985        if state.yaw - cyaw[0] >= PI {
2986            state.yaw -= 2.0 * PI;
2987        } else if state.yaw - cyaw[0] <= -PI {
2988            state.yaw += 2.0 * PI;
2989        }
2990
2991        let mut target_index = 0;
2992        let final_index = cx.len() - 1;
2993        let goal = (cx[final_index], cy[final_index]);
2994        let mut warm_start = vec![Vector2::zeros(); T];
2995        let mut recorded = Vec::new();
2996
2997        for step in 0..max_sim_steps {
2998            let (xref, new_index) = calc_ref_trajectory(
2999                &state,
3000                &cx,
3001                &cy,
3002                &cyaw,
3003                &speed_profile,
3004                UPSTREAM_SWITCH_BACK_TICK,
3005                target_index,
3006            );
3007            target_index = new_index;
3008
3009            let mpc_result = iterative_linear_mpc_control_with_test_optimizer(
3010                &xref,
3011                &state,
3012                &warm_start,
3013                optimize,
3014            );
3015            let accel = mpc_result.controls[0][0];
3016            let steer = mpc_result.controls[0][1];
3017
3018            state.update(accel, steer);
3019            warm_start = next_warm_start(&mpc_result.controls);
3020
3021            let dx = state.x - goal.0;
3022            let dy = state.y - goal.1;
3023            let goal_distance = (dx * dx + dy * dy).sqrt();
3024            let heading_error = angle_diff(state.yaw, cyaw[target_index]);
3025            let motion_curvature = steer.tan() / WB;
3026            let course_curvature = course_curvature_at_index(&cx, &cy, &cyaw, target_index);
3027            let near_goal = goal_distance <= GOAL_DIS;
3028            let near_end = final_index.abs_diff(target_index) < 5;
3029            let stopped = state.v.abs() <= STOP_SPEED;
3030            let reached_goal = near_goal && near_end && stopped;
3031
3032            if checkpoints.contains(&(step + 1)) {
3033                recorded.push(SwitchBackGoalControlSnapshot {
3034                    step: step + 1,
3035                    target_index,
3036                    goal_distance,
3037                    speed: state.v,
3038                    accel,
3039                    steer,
3040                    heading_error,
3041                    motion_curvature,
3042                    course_curvature,
3043                    near_goal,
3044                    near_end,
3045                    stopped,
3046                    reached_goal,
3047                });
3048            }
3049
3050            if reached_goal {
3051                break;
3052            }
3053        }
3054
3055        recorded
3056    }
3057
3058    fn goal_distance_delta(
3059        default: &SwitchBackClosedLoopCheckpoint,
3060        expanded: &SwitchBackClosedLoopCheckpoint,
3061    ) -> f64 {
3062        default.goal_distance - expanded.goal_distance
3063    }
3064
3065    fn checkpoint_at_step(
3066        checkpoints: &[SwitchBackClosedLoopCheckpoint],
3067        step: usize,
3068    ) -> &SwitchBackClosedLoopCheckpoint {
3069        checkpoints
3070            .iter()
3071            .find(|checkpoint| checkpoint.step == step)
3072            .unwrap_or_else(|| panic!("missing checkpoint for step {}", step))
3073    }
3074
3075    fn goal_distance_slope(
3076        start: &SwitchBackClosedLoopCheckpoint,
3077        end: &SwitchBackClosedLoopCheckpoint,
3078    ) -> f64 {
3079        let span = (end.step - start.step) as f64;
3080        (start.goal_distance - end.goal_distance) / span
3081    }
3082
3083    fn target_index_increment(
3084        start: &SwitchBackClosedLoopCheckpoint,
3085        end: &SwitchBackClosedLoopCheckpoint,
3086    ) -> isize {
3087        end.target_index as isize - start.target_index as isize
3088    }
3089
3090    fn checkpoint_lateral_error(
3091        checkpoint: &SwitchBackClosedLoopCheckpoint,
3092        cx: &[f64],
3093        cy: &[f64],
3094        cyaw: &[f64],
3095    ) -> f64 {
3096        let index = checkpoint.target_index.min(cx.len().saturating_sub(1));
3097        let dx = checkpoint.state[0] - cx[index];
3098        let dy = checkpoint.state[1] - cy[index];
3099        -dx * cyaw[index].sin() + dy * cyaw[index].cos()
3100    }
3101
3102    fn checkpoint_heading_error(checkpoint: &SwitchBackClosedLoopCheckpoint, cyaw: &[f64]) -> f64 {
3103        let index = checkpoint.target_index.min(cyaw.len().saturating_sub(1));
3104        angle_diff(checkpoint.state[3], cyaw[index])
3105    }
3106
3107    fn course_curvature_at_index(cx: &[f64], cy: &[f64], cyaw: &[f64], index: usize) -> f64 {
3108        if cx.len() < 3 || cy.len() < 3 || cyaw.len() < 3 {
3109            return 0.0;
3110        }
3111
3112        let i = index.clamp(1, cx.len().saturating_sub(2));
3113        let ds_prev = ((cx[i] - cx[i - 1]).powi(2) + (cy[i] - cy[i - 1]).powi(2))
3114            .sqrt()
3115            .max(f64::EPSILON);
3116        let ds_next = ((cx[i + 1] - cx[i]).powi(2) + (cy[i + 1] - cy[i]).powi(2))
3117            .sqrt()
3118            .max(f64::EPSILON);
3119
3120        angle_diff(cyaw[i + 1], cyaw[i - 1]) / (ds_prev + ds_next)
3121    }
3122
3123    fn scalar_slope(start: f64, end: f64, steps: usize) -> f64 {
3124        (end - start) / steps as f64
3125    }
3126
3127    fn goal_snapshot_at_step(
3128        snapshots: &[SwitchBackGoalConditionSnapshot],
3129        step: usize,
3130    ) -> &SwitchBackGoalConditionSnapshot {
3131        snapshots
3132            .iter()
3133            .find(|snapshot| snapshot.step == step)
3134            .unwrap_or_else(|| panic!("missing goal-condition snapshot for step {}", step))
3135    }
3136
3137    fn first_goal_condition_step<F>(
3138        snapshots: &[SwitchBackGoalConditionSnapshot],
3139        predicate: F,
3140    ) -> Option<usize>
3141    where
3142        F: Fn(&SwitchBackGoalConditionSnapshot) -> bool,
3143    {
3144        snapshots
3145            .iter()
3146            .find(|snapshot| predicate(snapshot))
3147            .map(|s| s.step)
3148    }
3149
3150    fn goal_control_snapshot_at_step(
3151        snapshots: &[SwitchBackGoalControlSnapshot],
3152        step: usize,
3153    ) -> &SwitchBackGoalControlSnapshot {
3154        snapshots
3155            .iter()
3156            .find(|snapshot| snapshot.step == step)
3157            .unwrap_or_else(|| panic!("missing goal-control snapshot for step {}", step))
3158    }
3159
3160    fn first_goal_control_condition_step<F>(
3161        snapshots: &[SwitchBackGoalControlSnapshot],
3162        predicate: F,
3163    ) -> Option<usize>
3164    where
3165        F: Fn(&SwitchBackGoalControlSnapshot) -> bool,
3166    {
3167        snapshots
3168            .iter()
3169            .find(|snapshot| predicate(snapshot))
3170            .map(|snapshot| snapshot.step)
3171    }
3172
3173    fn detailed_row_at_step(rows: &[SwitchBackDetailedRow], step: usize) -> &SwitchBackDetailedRow {
3174        rows.iter()
3175            .find(|row| row.step == step)
3176            .unwrap_or_else(|| panic!("missing detailed row for step {}", step))
3177    }
3178
3179    fn planning_snapshot_at_step(
3180        snapshots: &[SwitchBackPlanningSnapshot],
3181        step: usize,
3182    ) -> &SwitchBackPlanningSnapshot {
3183        snapshots
3184            .iter()
3185            .find(|snapshot| snapshot.step == step)
3186            .unwrap_or_else(|| panic!("missing planning snapshot for step {}", step))
3187    }
3188
3189    fn assert_planning_snapshot_phase_alignment(
3190        label: &str,
3191        default: &SwitchBackPlanningSnapshot,
3192        expanded: &SwitchBackPlanningSnapshot,
3193    ) {
3194        assert_eq!(
3195            default.target_index, expanded.target_index,
3196            "{} target-index alignment moved unexpectedly: default={:?} expanded={:?}",
3197            label, default, expanded
3198        );
3199        assert_diff_bounds(
3200            &format!("{label}_state"),
3201            trace_state_diff(&default.state, &expanded.state),
3202            [2e-4, 2e-4, 1e-4, 1e-4],
3203        );
3204        assert_diff_bounds(
3205            &format!("{label}_xref_head"),
3206            trace_state_diff(&default.xref_head, &expanded.xref_head),
3207            [1e-10; 4],
3208        );
3209        assert_diff_bounds(
3210            &format!("{label}_warm_start"),
3211            trace_control_diff(&default.warm_start_head, &expanded.warm_start_head),
3212            [1e-10, 3e-4],
3213        );
3214    }
3215
3216    fn assert_goal_control_alignment(
3217        default: &SwitchBackGoalControlSnapshot,
3218        expanded: &SwitchBackGoalControlSnapshot,
3219    ) {
3220        assert_eq!(default.target_index, expanded.target_index);
3221        assert_eq!(default.near_goal, expanded.near_goal);
3222        assert_eq!(default.near_end, expanded.near_end);
3223        assert_eq!(default.stopped, expanded.stopped);
3224        assert_eq!(default.reached_goal, expanded.reached_goal);
3225        assert!(
3226            (default.goal_distance - expanded.goal_distance).abs() <= 3.0e-4,
3227            "goal distance alignment moved unexpectedly: default={:?} expanded={:?}",
3228            default,
3229            expanded
3230        );
3231        assert!(
3232            (default.speed - expanded.speed).abs() <= 3.0e-4,
3233            "speed alignment moved unexpectedly: default={:?} expanded={:?}",
3234            default,
3235            expanded
3236        );
3237        assert!(
3238            (default.accel - expanded.accel).abs() <= 2.0e-3,
3239            "accel alignment moved unexpectedly: default={:?} expanded={:?}",
3240            default,
3241            expanded
3242        );
3243        assert!(
3244            (default.steer - expanded.steer).abs() <= 1.0e-4,
3245            "steer alignment moved unexpectedly: default={:?} expanded={:?}",
3246            default,
3247            expanded
3248        );
3249        assert!(
3250            (default.heading_error - expanded.heading_error).abs() <= 5.0e-5,
3251            "heading-error alignment moved unexpectedly: default={:?} expanded={:?}",
3252            default,
3253            expanded
3254        );
3255        assert!(
3256            (default.motion_curvature - expanded.motion_curvature).abs() <= 5.0e-5,
3257            "motion-curvature alignment moved unexpectedly: default={:?} expanded={:?}",
3258            default,
3259            expanded
3260        );
3261        assert!(
3262            (default.course_curvature - expanded.course_curvature).abs() <= 1.0e-12,
3263            "course-curvature alignment moved unexpectedly: default={:?} expanded={:?}",
3264            default,
3265            expanded
3266        );
3267    }
3268
3269    fn assert_detailed_phase_alignment(
3270        label: &str,
3271        default: &SwitchBackDetailedRow,
3272        expanded: &SwitchBackDetailedRow,
3273    ) {
3274        assert_eq!(
3275            default.target_index, expanded.target_index,
3276            "{} target-index alignment moved unexpectedly: default={:?} expanded={:?}",
3277            label, default, expanded
3278        );
3279
3280        let expanded_xref_rows = expanded
3281            .xref
3282            .iter()
3283            .map(|vec| [vec[0], vec[1], vec[2], vec[3]])
3284            .collect::<Vec<_>>();
3285        let expanded_control_rows = expanded
3286            .controls
3287            .iter()
3288            .map(|vec| [vec[0], vec[1]])
3289            .collect::<Vec<_>>();
3290        let expanded_predicted_rows = expanded
3291            .predicted
3292            .iter()
3293            .map(|vec| [vec[0], vec[1], vec[2], vec[3]])
3294            .collect::<Vec<_>>();
3295
3296        assert_diff_bounds(
3297            &format!("{label}_state"),
3298            trace_state_diff(&default.state, &expanded.state),
3299            [5e-4, 5e-4, 5e-4, 2e-4],
3300        );
3301        assert_diff_bounds(
3302            &format!("{label}_xref"),
3303            summarize_vec4_sequence_diff(&default.xref, &expanded_xref_rows),
3304            [1e-10; 4],
3305        );
3306        assert_diff_bounds(
3307            &format!("{label}_controls"),
3308            summarize_vec2_sequence_diff(&default.controls, &expanded_control_rows),
3309            [2e-3, 2e-4],
3310        );
3311        assert_diff_bounds(
3312            &format!("{label}_predicted"),
3313            summarize_vec4_sequence_diff(&default.predicted, &expanded_predicted_rows),
3314            [7e-4, 7e-4, 7e-4, 2e-4],
3315        );
3316    }
3317
3318    #[test]
3319    fn test_speed_profile_stops_at_goal() {
3320        let cx = vec![0.0, 1.0, 2.0];
3321        let cy = vec![0.0, 0.0, 0.0];
3322        let cyaw = vec![0.0, 0.0, 0.0];
3323        let speed = calc_speed_profile(&cx, &cy, &cyaw, TARGET_SPEED);
3324
3325        assert_eq!(speed.len(), 3);
3326        assert_eq!(speed[2], 0.0);
3327        assert!(speed[0] > 0.0);
3328    }
3329
3330    #[test]
3331    fn test_iterative_linear_mpc_matches_reference_offset_state() {
3332        // Reference generated from the PythonRobotics MPC equations and constraints
3333        // using SciPy SLSQP for the fixed scenario below.
3334        let xref = vec![
3335            Vector4::new(0.0, 0.0, TARGET_SPEED, 0.0),
3336            Vector4::new(0.5, 0.0, TARGET_SPEED, 0.0),
3337            Vector4::new(1.0, 0.0, TARGET_SPEED, 0.0),
3338            Vector4::new(1.5, 0.0, TARGET_SPEED, 0.0),
3339            Vector4::new(2.0, 0.0, TARGET_SPEED, 0.0),
3340            Vector4::new(2.5, 0.0, TARGET_SPEED, 0.0),
3341        ];
3342        let state = State::new(0.0, 0.3, 1.0, 0.1);
3343
3344        let result = iterative_linear_mpc_control(&xref, &state, &[]);
3345
3346        let expected_controls = [
3347            [1.0, -0.669_667_215_071],
3348            [1.0, -0.630_794_908_167],
3349            [1.0, -0.577_878_962_843],
3350            [1.0, -0.534_142_026_172],
3351            [1.0, -0.513_091_902_297],
3352        ];
3353        for (actual, expected) in result.controls.iter().zip(expected_controls.iter()) {
3354            assert_vec2_close(actual, *expected, 5e-3);
3355        }
3356
3357        let final_predicted = result.predicted.last().unwrap();
3358        assert_vec4_close(
3359            final_predicted,
3360            [1.394_401_496_574, 0.249_081_964_95, 2.0, -0.221_107_545_509],
3361            5e-2,
3362        );
3363    }
3364
3365    #[test]
3366    fn test_mpc_simulation_reaches_goal() {
3367        let ax = vec![0.0, 6.0, 12.0];
3368        let ay = vec![0.0, 0.0, 0.0];
3369        let result = run_mpc_simulation_with_course(&ax, &ay, 120);
3370        let goal_distance = ((result.final_state.x - result.goal.0).powi(2)
3371            + (result.final_state.y - result.goal.1).powi(2))
3372        .sqrt();
3373
3374        assert!(result.reached_goal);
3375        assert!(goal_distance <= GOAL_DIS + 0.25);
3376        assert!(result.hist_x.len() < 120);
3377    }
3378
3379    #[test]
3380    fn test_mpc_straight_course_closed_loop_matches_reference() {
3381        // Reference generated from the PythonRobotics equations with the same
3382        // straight-course setup, solved by SciPy SLSQP over the constrained QP.
3383        let ax = vec![0.0, 6.0, 12.0];
3384        let ay = vec![0.0, 0.0, 0.0];
3385        let result = run_mpc_simulation_with_course(&ax, &ay, 120);
3386
3387        assert!(result.reached_goal);
3388        assert_eq!(result.target_index, result.final_index);
3389        assert_eq!(result.target_index, 24);
3390        assert_eq!(result.hist_x.len(), 56);
3391
3392        assert_vec4_close(
3393            &result.final_state.to_vector(),
3394            [
3395                12.126_756_016_762_242,
3396                0.000_001_043_289_690_068_484_6,
3397                -0.123_832_283_912_188_83,
3398                0.000_000_381_531_819_982_818_5,
3399            ],
3400            1e-2,
3401        );
3402    }
3403
3404    #[test]
3405    fn test_switch_back_course_contains_reverse_segment() {
3406        let (cx, cy, cyaw) = generate_switch_back_course();
3407        let speed_profile = calc_speed_profile(&cx, &cy, &cyaw, TARGET_SPEED);
3408
3409        assert_eq!(cx.first().copied(), Some(0.0));
3410        assert_eq!(cy.first().copied(), Some(0.0));
3411        assert_eq!(cx.len(), 162);
3412        assert!(cy.iter().copied().fold(f64::NEG_INFINITY, f64::max) > 30.0);
3413        assert!((cx.last().unwrap() + 0.016_802_167_501_358_26).abs() <= 1e-9);
3414        assert!((cy.last().unwrap() - 0.833_302_319_727_219_5).abs() <= 1e-9);
3415        assert!(speed_profile[..speed_profile.len().saturating_sub(1)]
3416            .iter()
3417            .any(|speed| *speed < 0.0));
3418        assert_eq!(speed_profile.last().copied(), Some(0.0));
3419    }
3420
3421    #[test]
3422    fn test_mpc_switch_back_midcourse_window_matches_reference() {
3423        // Reference generated from PythonRobotics switch-back simulation
3424        // at closed-loop step 20 using cvxpy CLARABEL.
3425        let ctx = advance_switch_back_context(20);
3426        let (xref, target_index) = calc_ref_trajectory(
3427            &ctx.state,
3428            &ctx.cx,
3429            &ctx.cy,
3430            &ctx.cyaw,
3431            &ctx.speed_profile,
3432            UPSTREAM_SWITCH_BACK_TICK,
3433            ctx.target_index,
3434        );
3435        let result = iterative_linear_mpc_control(&xref, &ctx.state, &ctx.warm_start);
3436
3437        assert_eq!(target_index, 5);
3438        assert_vec4_close(
3439            &ctx.state.to_vector(),
3440            [
3441                7.528_990_211_684_919,
3442                -0.616_644_989_009_243_4,
3443                3.855_895_792_930_033_5,
3444                -0.078_281_934_138_131_24,
3445            ],
3446            1e-2,
3447        );
3448
3449        let expected_xref = [
3450            [
3451                7.884_351_723_374_65,
3452                -0.646_692_295_268_678_9,
3453                TARGET_SPEED,
3454                -0.078_822_806_954_514_1,
3455            ],
3456            [
3457                9.417_709_562_051_241,
3458                -0.766_274_939_125_219_6,
3459                TARGET_SPEED,
3460                -0.076_759_688_257_237_88,
3461            ],
3462            [
3463                10.927_333_306_546_922,
3464                -0.880_536_229_237_835_5,
3465                TARGET_SPEED,
3466                -0.074_243_230_035_395_7,
3467            ],
3468            [
3469                10.927_333_306_546_922,
3470                -0.880_536_229_237_835_5,
3471                TARGET_SPEED,
3472                -0.074_243_230_035_395_7,
3473            ],
3474            [
3475                12.409_267_274_498_205,
3476                -0.988_589_273_315_872_4,
3477                TARGET_SPEED,
3478                -0.071_228_758_450_065_36,
3479            ],
3480            [
3481                13.859_555_783_541_607,
3482                -1.089_547_179_068_676,
3483                TARGET_SPEED,
3484                -0.067_659_809_911_111_4,
3485            ],
3486        ];
3487        for (actual, expected) in xref.iter().zip(expected_xref.iter()) {
3488            assert_vec4_close(actual, *expected, 1e-3);
3489        }
3490
3491        let expected_controls = [
3492            [0.999_999_995_100_728_5, 0.004_502_534_498_214_272],
3493            [0.619_401_970_887_073, 0.003_626_365_725_009_755],
3494            [-0.999_999_586_012_278, 0.003_497_926_790_795_169],
3495            [-0.999_999_988_263_528_5, 0.004_178_759_548_203_836],
3496            [-0.999_999_985_187_766_1, 0.004_753_969_705_983_846],
3497        ];
3498        for (actual, expected) in result.controls.iter().zip(expected_controls.iter()) {
3499            assert_vec2_close(actual, *expected, 2e-2);
3500        }
3501
3502        let final_predicted = result.predicted.last().unwrap();
3503        assert_vec4_close(
3504            final_predicted,
3505            [
3506                11.487_827_888_938_794,
3507                -0.917_080_324_028_499_6,
3508                3.579_776_274_235_015_4,
3509                -0.071_778_774_991_017_19,
3510            ],
3511            5e-2,
3512        );
3513    }
3514
3515    #[test]
3516    fn test_mpc_switch_back_prefix_trace_matches_pythonrobotics_reference() {
3517        let expected =
3518            parse_switch_back_trace(include_str!("testdata/mpc_switch_back_prefix_trace.csv"));
3519        // Steps 0..=70 stay on the pre-reversal branch and are stable enough for
3520        // step-by-step parity. The reverse transition is covered separately by
3521        // the midcourse window and full-course regressions.
3522        let strict_prefix_steps = 71;
3523        let expected = &expected[..strict_prefix_steps];
3524        let actual = collect_switch_back_prefix_trace(expected.len());
3525        let summary = summarize_switch_back_trace_window(&actual, expected);
3526
3527        assert_eq!(summary.max_target_index_gap, 0);
3528        assert_diff_bounds("state", summary.max_state_diff, [4e-3, 3e-3, 4e-3, 2e-3]);
3529        assert_diff_bounds("xref_head", summary.max_xref_head_diff, [1e-10; 4]);
3530        assert_diff_bounds("xref_tail", summary.max_xref_tail_diff, [1e-10; 4]);
3531        assert_diff_bounds("control", summary.max_control_diff, [1e-2, 6e-3]);
3532        assert_diff_bounds(
3533            "predicted_tail",
3534            summary.max_predicted_tail_diff,
3535            [0.11, 0.13, 5e-3, 0.17],
3536        );
3537    }
3538
3539    #[test]
3540    fn test_mpc_switch_back_reverse_transition_matches_pythonrobotics_reference() {
3541        let expected =
3542            parse_switch_back_trace(include_str!("testdata/mpc_switch_back_prefix_trace.csv"));
3543        // The reverse handoff is the point where the projected-gradient solver
3544        // starts to drift from the upstream cvxpy reference. Keep it under a
3545        // bounded window even though step-by-step strict parity is not stable.
3546        let reverse_window = 71..81;
3547        let actual = collect_switch_back_prefix_trace(reverse_window.end);
3548        let summary = summarize_switch_back_trace_window(
3549            &actual[reverse_window.clone()],
3550            &expected[reverse_window],
3551        );
3552
3553        assert!(summary.max_target_index_gap <= 1);
3554        assert_diff_bounds(
3555            "reverse_state",
3556            summary.max_state_diff,
3557            [0.04, 0.35, 0.17, 0.2],
3558        );
3559        assert_diff_bounds(
3560            "reverse_xref_head",
3561            summary.max_xref_head_diff,
3562            [0.75, 0.45, 1e-12, 0.02],
3563        );
3564        assert_diff_bounds(
3565            "reverse_xref_tail",
3566            summary.max_xref_tail_diff,
3567            [0.95, 0.5, 1e-12, 0.01],
3568        );
3569        assert_diff_bounds("reverse_control", summary.max_control_diff, [0.86, 0.41]);
3570        assert_diff_bounds(
3571            "reverse_predicted_tail",
3572            summary.max_predicted_tail_diff,
3573            [0.26, 0.62, 0.36, 0.55],
3574        );
3575    }
3576
3577    #[test]
3578    fn test_mpc_switch_back_reverse_detailed_trace_matches_pythonrobotics_reference() {
3579        let expected_trace =
3580            parse_switch_back_trace(include_str!("testdata/mpc_switch_back_prefix_trace.csv"));
3581        let expected_controls = parse_switch_back_reverse_controls(include_str!(
3582            "testdata/mpc_switch_back_reverse_controls.csv"
3583        ));
3584        let expected_horizon = parse_switch_back_reverse_horizon(include_str!(
3585            "testdata/mpc_switch_back_reverse_horizon.csv"
3586        ));
3587        let actual_trace = collect_switch_back_detailed_trace(81);
3588        let summary = summarize_switch_back_detailed_window(
3589            &actual_trace,
3590            &expected_trace,
3591            &expected_controls,
3592            &expected_horizon,
3593            71..=80,
3594        );
3595
3596        assert!(summary.max_target_index_gap <= 1);
3597        assert_diff_bounds(
3598            "reverse_detailed_state",
3599            summary.max_state_diff,
3600            [0.04, 0.35, 0.17, 0.21],
3601        );
3602        assert_diff_bounds(
3603            "reverse_detailed_xref",
3604            summary.max_xref_diff,
3605            [0.92, 0.49, 1e-12, 0.02],
3606        );
3607        assert_diff_bounds(
3608            "reverse_detailed_control",
3609            summary.max_control_diff,
3610            [1.46, 0.5],
3611        );
3612        assert_diff_bounds(
3613            "reverse_detailed_predicted",
3614            summary.max_predicted_diff,
3615            [0.26, 0.62, 0.47, 0.55],
3616        );
3617    }
3618
3619    #[test]
3620    fn test_mpc_switch_back_reverse_full_candidate_expansion_characterizes_detailed_trace_tradeoff()
3621    {
3622        let expected_trace =
3623            parse_switch_back_trace(include_str!("testdata/mpc_switch_back_prefix_trace.csv"));
3624        let expected_controls = parse_switch_back_reverse_controls(include_str!(
3625            "testdata/mpc_switch_back_reverse_controls.csv"
3626        ));
3627        let expected_horizon = parse_switch_back_reverse_horizon(include_str!(
3628            "testdata/mpc_switch_back_reverse_horizon.csv"
3629        ));
3630        let actual_trace = collect_switch_back_detailed_trace_with_test_optimizer(
3631            81,
3632            optimize_linearized_controls_with_full_candidate_expansion,
3633        );
3634        let default_summary = summarize_switch_back_detailed_window(
3635            &collect_switch_back_detailed_trace(81),
3636            &expected_trace,
3637            &expected_controls,
3638            &expected_horizon,
3639            71..=80,
3640        );
3641        let expanded_summary = summarize_switch_back_detailed_window(
3642            &actual_trace,
3643            &expected_trace,
3644            &expected_controls,
3645            &expected_horizon,
3646            71..=80,
3647        );
3648
3649        assert!(expanded_summary.max_target_index_gap <= 1);
3650        assert_eq!(
3651            expanded_summary.max_xref_diff,
3652            default_summary.max_xref_diff
3653        );
3654        assert_diff_bounds(
3655            "reverse_detailed_full_candidates_state_characterization",
3656            expanded_summary.max_state_diff,
3657            [0.043, 0.35, 0.17, 0.21],
3658        );
3659        assert_diff_bounds(
3660            "reverse_detailed_full_candidates_control_characterization",
3661            expanded_summary.max_control_diff,
3662            [1.32, 0.5],
3663        );
3664        assert_diff_bounds(
3665            "reverse_detailed_full_candidates_predicted_characterization",
3666            expanded_summary.max_predicted_diff,
3667            [0.22, 0.62, 0.41, 0.43],
3668        );
3669
3670        // After hybrid QP+PG solver promotion, default and expanded may differ
3671        // slightly due to candidate expansion interacting with the hybrid solver.
3672        // Verify they remain within a reasonable margin of each other.
3673        for (i, (d, e)) in default_summary
3674            .max_state_diff
3675            .iter()
3676            .zip(expanded_summary.max_state_diff.iter())
3677            .enumerate()
3678        {
3679            assert!(
3680                (d - e).abs() < 0.2,
3681                "state diff index {} diverged too much: default={} expanded={}",
3682                i,
3683                d,
3684                e
3685            );
3686        }
3687        for (i, (d, e)) in default_summary
3688            .max_control_diff
3689            .iter()
3690            .zip(expanded_summary.max_control_diff.iter())
3691            .enumerate()
3692        {
3693            assert!(
3694                (d - e).abs() < 0.2,
3695                "control diff index {} diverged too much: default={} expanded={}",
3696                i,
3697                d,
3698                e
3699            );
3700        }
3701    }
3702
3703    #[test]
3704    fn test_mpc_switch_back_reverse_solver_gap_with_python_warm_start() {
3705        let expected_trace =
3706            parse_switch_back_trace(include_str!("testdata/mpc_switch_back_prefix_trace.csv"));
3707        let expected_controls = parse_switch_back_reverse_controls(include_str!(
3708            "testdata/mpc_switch_back_reverse_controls.csv"
3709        ));
3710        let expected_horizon = parse_switch_back_reverse_horizon(include_str!(
3711            "testdata/mpc_switch_back_reverse_horizon.csv"
3712        ));
3713
3714        for step in [75usize, 78usize] {
3715            let expected_state = &expected_trace[step].state;
3716            let python_warm_start = expected_controls.get(&(step - 1)).unwrap();
3717            let expected_controls_for_step = expected_controls.get(&step).unwrap();
3718            let (expected_xref_for_step, expected_predicted_for_step) =
3719                expected_horizon.get(&step).unwrap();
3720
3721            let expected_control_rows = expected_controls_for_step
3722                .iter()
3723                .map(|vec| [vec[0], vec[1]])
3724                .collect::<Vec<_>>();
3725            let expected_predicted_rows = expected_predicted_for_step
3726                .iter()
3727                .map(|vec| [vec[0], vec[1], vec[2], vec[3]])
3728                .collect::<Vec<_>>();
3729
3730            let result = iterative_linear_mpc_control(
3731                expected_xref_for_step,
3732                &state_from_vector4(expected_state),
3733                python_warm_start,
3734            );
3735
3736            let control_diff =
3737                summarize_vec2_sequence_diff(&result.controls, &expected_control_rows);
3738            let predicted_diff =
3739                summarize_vec4_sequence_diff(&result.predicted, &expected_predicted_rows);
3740            let (control_tol, predicted_tol) = match step {
3741                75 => ([0.38, 0.11], [0.09, 0.23, 0.11, 0.18]),
3742                78 => ([0.52, 0.35], [0.03, 0.46, 0.11, 0.25]),
3743                _ => unreachable!(),
3744            };
3745
3746            assert_diff_bounds(
3747                &format!("python_warm_start_step_{step}_control"),
3748                control_diff,
3749                control_tol,
3750            );
3751            assert_diff_bounds(
3752                &format!("python_warm_start_step_{step}_predicted"),
3753                predicted_diff,
3754                predicted_tol,
3755            );
3756        }
3757    }
3758
3759    #[test]
3760    fn test_mpc_switch_back_predict_motion_matches_python_warm_start_reference() {
3761        let expected_trace =
3762            parse_switch_back_trace(include_str!("testdata/mpc_switch_back_prefix_trace.csv"));
3763        let expected_controls = parse_switch_back_reverse_controls(include_str!(
3764            "testdata/mpc_switch_back_reverse_controls.csv"
3765        ));
3766        let expected_xbar = parse_switch_back_reverse_states(include_str!(
3767            "testdata/mpc_switch_back_reverse_xbar.csv"
3768        ));
3769
3770        for step in [75usize, 78usize] {
3771            let expected_state = &expected_trace[step].state;
3772            let python_warm_start = expected_controls.get(&(step - 1)).unwrap();
3773            let expected_xbar_for_step = expected_xbar.get(&step).unwrap();
3774            let actual_xbar = predict_motion(state_from_vector4(expected_state), python_warm_start);
3775            let xbar_diff = summarize_vec4_sequence_diff(
3776                &actual_xbar,
3777                &expected_xbar_for_step
3778                    .iter()
3779                    .map(|vec| [vec[0], vec[1], vec[2], vec[3]])
3780                    .collect::<Vec<_>>(),
3781            );
3782
3783            assert_diff_bounds(
3784                &format!("python_warm_start_step_{step}_xbar"),
3785                xbar_diff,
3786                [1e-9, 1e-9, 1e-9, 1e-9],
3787            );
3788        }
3789    }
3790
3791    #[test]
3792    fn test_mpc_switch_back_reverse_linearized_cost_gap_against_python_reference() {
3793        let expected_trace =
3794            parse_switch_back_trace(include_str!("testdata/mpc_switch_back_prefix_trace.csv"));
3795        let expected_controls = parse_switch_back_reverse_controls(include_str!(
3796            "testdata/mpc_switch_back_reverse_controls.csv"
3797        ));
3798        let expected_horizon = parse_switch_back_reverse_horizon(include_str!(
3799            "testdata/mpc_switch_back_reverse_horizon.csv"
3800        ));
3801        let expected_xbar = parse_switch_back_reverse_states(include_str!(
3802            "testdata/mpc_switch_back_reverse_xbar.csv"
3803        ));
3804
3805        for step in [75usize, 78usize] {
3806            let expected_state = &expected_trace[step].state;
3807            let python_warm_start = expected_controls.get(&(step - 1)).unwrap();
3808            let python_controls = expected_controls.get(&step).unwrap();
3809            let (expected_xref_for_step, expected_predicted_for_step) =
3810                expected_horizon.get(&step).unwrap();
3811            let expected_xbar_for_step = expected_xbar.get(&step).unwrap();
3812            let state = state_from_vector4(expected_state);
3813
3814            let rust_controls = optimize_linearized_controls(
3815                expected_xref_for_step,
3816                expected_xbar_for_step,
3817                &state,
3818                python_warm_start,
3819            );
3820            let python_x_python_style = linearized_rollout_without_angle_wrap(
3821                &state,
3822                expected_xbar_for_step,
3823                python_controls,
3824            );
3825            let python_cost_python_style = compute_cost_without_angle_wrap(
3826                &python_x_python_style,
3827                expected_xref_for_step,
3828                python_controls,
3829            );
3830
3831            let rust_x_python_style = linearized_rollout_without_angle_wrap(
3832                &state,
3833                expected_xbar_for_step,
3834                &rust_controls,
3835            );
3836            let rust_cost_python_style = compute_cost_without_angle_wrap(
3837                &rust_x_python_style,
3838                expected_xref_for_step,
3839                &rust_controls,
3840            );
3841            let predicted_diff_python_style = summarize_vec4_sequence_diff(
3842                &python_x_python_style,
3843                &expected_predicted_for_step
3844                    .iter()
3845                    .map(|vec| [vec[0], vec[1], vec[2], vec[3]])
3846                    .collect::<Vec<_>>(),
3847            );
3848            let predicted_tol = match step {
3849                75 => [0.11, 0.07, 1e-12, 0.002],
3850                78 => [0.06, 0.02, 1e-12, 0.006],
3851                _ => unreachable!(),
3852            };
3853
3854            assert_diff_bounds(
3855                &format!("python_style_linearized_rollout_step_{step}"),
3856                predicted_diff_python_style,
3857                predicted_tol,
3858            );
3859
3860            let solver_gap = rust_cost_python_style - python_cost_python_style;
3861            let min_gap = match step {
3862                75 => 1.0,
3863                // Hybrid QP+PG solver produces near-parity cost at step 78
3864                78 => -0.1,
3865                _ => unreachable!(),
3866            };
3867            assert!(
3868                solver_gap > min_gap,
3869                "step {} solver gap too small: rust={} python={} gap={}",
3870                step,
3871                rust_cost_python_style,
3872                python_cost_python_style,
3873                solver_gap
3874            );
3875        }
3876    }
3877
3878    #[test]
3879    fn test_mpc_switch_back_reverse_python_style_solver_trace() {
3880        let expected_trace =
3881            parse_switch_back_trace(include_str!("testdata/mpc_switch_back_prefix_trace.csv"));
3882        let expected_controls = parse_switch_back_reverse_controls(include_str!(
3883            "testdata/mpc_switch_back_reverse_controls.csv"
3884        ));
3885        let expected_horizon = parse_switch_back_reverse_horizon(include_str!(
3886            "testdata/mpc_switch_back_reverse_horizon.csv"
3887        ));
3888        let expected_xbar = parse_switch_back_reverse_states(include_str!(
3889            "testdata/mpc_switch_back_reverse_xbar.csv"
3890        ));
3891
3892        for step in [75usize, 78usize] {
3893            let expected_state = &expected_trace[step].state;
3894            let python_warm_start = expected_controls.get(&(step - 1)).unwrap();
3895            let python_controls = expected_controls.get(&step).unwrap();
3896            let (expected_xref_for_step, _) = expected_horizon.get(&step).unwrap();
3897            let expected_xbar_for_step = expected_xbar.get(&step).unwrap();
3898            let state = state_from_vector4(expected_state);
3899
3900            let (default_controls, default_trace) =
3901                optimize_linearized_controls_without_angle_wrap_with_trace(
3902                    expected_xref_for_step,
3903                    expected_xbar_for_step,
3904                    &state,
3905                    python_warm_start,
3906                    QP_MAX_ITERS,
3907                    LINE_SEARCH_ITERS,
3908                );
3909            let (extended_controls, extended_trace) =
3910                optimize_linearized_controls_without_angle_wrap_with_trace(
3911                    expected_xref_for_step,
3912                    expected_xbar_for_step,
3913                    &state,
3914                    python_warm_start,
3915                    QP_MAX_ITERS * 10,
3916                    LINE_SEARCH_ITERS * 2,
3917                );
3918
3919            let python_x = linearized_rollout_without_angle_wrap(
3920                &state,
3921                expected_xbar_for_step,
3922                python_controls,
3923            );
3924            let python_cost =
3925                compute_cost_without_angle_wrap(&python_x, expected_xref_for_step, python_controls);
3926
3927            let default_x = linearized_rollout_without_angle_wrap(
3928                &state,
3929                expected_xbar_for_step,
3930                &default_controls,
3931            );
3932            let default_cost = compute_cost_without_angle_wrap(
3933                &default_x,
3934                expected_xref_for_step,
3935                &default_controls,
3936            );
3937
3938            let extended_x = linearized_rollout_without_angle_wrap(
3939                &state,
3940                expected_xbar_for_step,
3941                &extended_controls,
3942            );
3943            let extended_cost = compute_cost_without_angle_wrap(
3944                &extended_x,
3945                expected_xref_for_step,
3946                &extended_controls,
3947            );
3948
3949            let default_gap = default_cost - python_cost;
3950            let extended_gap = extended_cost - python_cost;
3951            let last_default_grad = default_trace
3952                .last()
3953                .map(|entry| entry.gradient_norm)
3954                .unwrap_or(0.0);
3955            let last_extended_grad = extended_trace
3956                .last()
3957                .map(|entry| entry.gradient_norm)
3958                .unwrap_or(0.0);
3959
3960            assert!(
3961                extended_cost <= default_cost + 1e-9,
3962                "step {} extended cost regressed: default={} extended={}",
3963                step,
3964                default_cost,
3965                extended_cost
3966            );
3967            assert!(
3968                default_trace
3969                    .iter()
3970                    .any(|entry| entry.accepted_step.is_some()),
3971                "step {} default trace never accepted a line-search step: {:?}",
3972                step,
3973                default_trace
3974            );
3975            assert!(
3976                extended_trace
3977                    .iter()
3978                    .any(|entry| entry.accepted_step.is_some()),
3979                "step {} extended trace never accepted a line-search step: {:?}",
3980                step,
3981                extended_trace
3982            );
3983            assert!(
3984                default_trace.len() >= 150,
3985                "step {} default trace terminated too early: {:?}",
3986                step,
3987                default_trace
3988            );
3989            assert!(
3990                extended_trace.len() >= default_trace.len(),
3991                "step {} extended trace shorter than default: default={} extended={}",
3992                step,
3993                default_trace.len(),
3994                extended_trace.len()
3995            );
3996            assert!(
3997                last_default_grad > 0.1 && last_extended_grad > 0.1,
3998                "step {} gradients converged unexpectedly: default={} extended={}",
3999                step,
4000                last_default_grad,
4001                last_extended_grad
4002            );
4003
4004            let min_default_cost = default_trace
4005                .iter()
4006                .map(|entry| entry.cost)
4007                .fold(f64::INFINITY, f64::min);
4008            let min_extended_cost = extended_trace
4009                .iter()
4010                .map(|entry| entry.cost)
4011                .fold(f64::INFINITY, f64::min);
4012            assert!(
4013                (default_cost - extended_cost) <= 1e-3,
4014                "step {} extended budget changed cost too much: default={} extended={} gap_default={} gap_extended={}",
4015                step,
4016                default_cost,
4017                extended_cost,
4018                default_gap,
4019                extended_gap
4020            );
4021            assert!(
4022                (min_default_cost - min_extended_cost) <= 1e-3,
4023                "step {} extended budget changed min trace cost too much: default={} extended={}",
4024                step,
4025                min_default_cost,
4026                min_extended_cost
4027            );
4028        }
4029    }
4030
4031    #[test]
4032    fn test_mpc_switch_back_reverse_line_search_scan_characterization() {
4033        let expected_trace =
4034            parse_switch_back_trace(include_str!("testdata/mpc_switch_back_prefix_trace.csv"));
4035        let expected_controls = parse_switch_back_reverse_controls(include_str!(
4036            "testdata/mpc_switch_back_reverse_controls.csv"
4037        ));
4038        let expected_horizon = parse_switch_back_reverse_horizon(include_str!(
4039            "testdata/mpc_switch_back_reverse_horizon.csv"
4040        ));
4041        let expected_xbar = parse_switch_back_reverse_states(include_str!(
4042            "testdata/mpc_switch_back_reverse_xbar.csv"
4043        ));
4044
4045        let step_75_state = state_from_vector4(&expected_trace[75].state);
4046        let step_75_scan = first_iteration_projection_candidates(
4047            expected_horizon.get(&75).unwrap().0.as_slice(),
4048            expected_xbar.get(&75).unwrap(),
4049            &step_75_state,
4050            expected_controls.get(&74).unwrap(),
4051            expected_controls.get(&75).unwrap(),
4052            &[0.25, 0.5, 1.0],
4053        )
4054        .1;
4055        let step_75_small = &step_75_scan[0];
4056        let step_75_medium = &step_75_scan[1];
4057        let step_75_large = &step_75_scan[2];
4058
4059        assert!(
4060            step_75_large.projected_cost + 0.05 < step_75_medium.projected_cost,
4061            "step 75 larger initial step did not improve projected cost enough: {:?}",
4062            step_75_scan
4063        );
4064        assert!(
4065            step_75_large.projected_cost + 0.25 < step_75_small.projected_cost,
4066            "step 75 larger initial step did not beat default projected cost enough: {:?}",
4067            step_75_scan
4068        );
4069        assert!(
4070            step_75_large.projected_to_python[1] + 0.05 < step_75_small.projected_to_python[1],
4071            "step 75 larger initial step did not move steering closer to python enough: {:?}",
4072            step_75_scan
4073        );
4074
4075        let step_78_state = state_from_vector4(&expected_trace[78].state);
4076        let step_78_scan = first_iteration_projection_candidates(
4077            expected_horizon.get(&78).unwrap().0.as_slice(),
4078            expected_xbar.get(&78).unwrap(),
4079            &step_78_state,
4080            expected_controls.get(&77).unwrap(),
4081            expected_controls.get(&78).unwrap(),
4082            &[0.25, 0.5, 1.0],
4083        )
4084        .1;
4085        let step_78_small = &step_78_scan[0];
4086        let step_78_medium = &step_78_scan[1];
4087        let step_78_large = &step_78_scan[2];
4088
4089        assert!(
4090            step_78_small.projected_cost + 0.15 < step_78_medium.projected_cost,
4091            "step 78 default initial step should remain better than 0.5: {:?}",
4092            step_78_scan
4093        );
4094        assert!(
4095            step_78_small.projected_cost + 0.2 < step_78_large.projected_cost,
4096            "step 78 default initial step should remain better than 1.0: {:?}",
4097            step_78_scan
4098        );
4099    }
4100
4101    #[test]
4102    fn test_mpc_switch_back_reverse_candidate_expansion_improves_python_style_cost() {
4103        let expected_trace =
4104            parse_switch_back_trace(include_str!("testdata/mpc_switch_back_prefix_trace.csv"));
4105        let expected_controls = parse_switch_back_reverse_controls(include_str!(
4106            "testdata/mpc_switch_back_reverse_controls.csv"
4107        ));
4108        let expected_horizon = parse_switch_back_reverse_horizon(include_str!(
4109            "testdata/mpc_switch_back_reverse_horizon.csv"
4110        ));
4111        let expected_xbar = parse_switch_back_reverse_states(include_str!(
4112            "testdata/mpc_switch_back_reverse_xbar.csv"
4113        ));
4114        let step_candidates = [1.0, 0.5, 0.25, 0.125, 0.0625];
4115
4116        for step in [75usize, 78usize] {
4117            let expected_state = &expected_trace[step].state;
4118            let python_warm_start = expected_controls.get(&(step - 1)).unwrap();
4119            let python_controls = expected_controls.get(&step).unwrap();
4120            let (expected_xref_for_step, _) = expected_horizon.get(&step).unwrap();
4121            let expected_xbar_for_step = expected_xbar.get(&step).unwrap();
4122            let state = state_from_vector4(expected_state);
4123
4124            let (default_controls, _) = optimize_linearized_controls_without_angle_wrap_with_trace(
4125                expected_xref_for_step,
4126                expected_xbar_for_step,
4127                &state,
4128                python_warm_start,
4129                QP_MAX_ITERS,
4130                LINE_SEARCH_ITERS,
4131            );
4132            let (expanded_controls, expanded_trace) =
4133                optimize_linearized_controls_without_angle_wrap_with_candidate_expansion(
4134                    expected_xref_for_step,
4135                    expected_xbar_for_step,
4136                    &state,
4137                    python_warm_start,
4138                    QP_MAX_ITERS,
4139                    &step_candidates,
4140                );
4141
4142            let python_x = linearized_rollout_without_angle_wrap(
4143                &state,
4144                expected_xbar_for_step,
4145                python_controls,
4146            );
4147            let python_cost =
4148                compute_cost_without_angle_wrap(&python_x, expected_xref_for_step, python_controls);
4149            let default_x = linearized_rollout_without_angle_wrap(
4150                &state,
4151                expected_xbar_for_step,
4152                &default_controls,
4153            );
4154            let default_cost = compute_cost_without_angle_wrap(
4155                &default_x,
4156                expected_xref_for_step,
4157                &default_controls,
4158            );
4159            let expanded_x = linearized_rollout_without_angle_wrap(
4160                &state,
4161                expected_xbar_for_step,
4162                &expanded_controls,
4163            );
4164            let expanded_cost = compute_cost_without_angle_wrap(
4165                &expanded_x,
4166                expected_xref_for_step,
4167                &expanded_controls,
4168            );
4169            let default_gap = default_cost - python_cost;
4170            let expanded_gap = expanded_cost - python_cost;
4171
4172            assert!(
4173                expanded_trace.iter().any(|entry| {
4174                    entry
4175                        .accepted_step
4176                        .is_some_and(|accepted| (accepted - 0.25).abs() > 1e-12)
4177                }),
4178                "step {} candidate expansion never used a non-default step: {:?}",
4179                step,
4180                expanded_trace
4181            );
4182
4183            match step {
4184                75 => {
4185                    assert!(
4186                        default_gap > 0.1,
4187                        "step 75 default gap regressed unexpectedly: {}",
4188                        default_gap
4189                    );
4190                    assert!(
4191                        expanded_gap < 0.0,
4192                        "step 75 candidate expansion did not beat python-style reference: {}",
4193                        expanded_gap
4194                    );
4195                    assert!(
4196                        expanded_cost + 0.1 < default_cost,
4197                        "step 75 candidate expansion did not improve enough: default={} expanded={}",
4198                        default_cost,
4199                        expanded_cost
4200                    );
4201                }
4202                78 => {
4203                    assert!(
4204                        expanded_cost <= default_cost + 1e-6,
4205                        "step 78 candidate expansion regressed: default={} expanded={}",
4206                        default_cost,
4207                        expanded_cost
4208                    );
4209                    assert!(
4210                        expanded_gap <= default_gap + 1e-6,
4211                        "step 78 candidate expansion widened gap: default_gap={} expanded_gap={}",
4212                        default_gap,
4213                        expanded_gap
4214                    );
4215                }
4216                _ => unreachable!(),
4217            }
4218        }
4219    }
4220
4221    #[test]
4222    fn test_mpc_switch_back_reverse_wrapped_candidate_expansion_needs_small_step_fallback() {
4223        let expected_trace =
4224            parse_switch_back_trace(include_str!("testdata/mpc_switch_back_prefix_trace.csv"));
4225        let expected_controls = parse_switch_back_reverse_controls(include_str!(
4226            "testdata/mpc_switch_back_reverse_controls.csv"
4227        ));
4228        let expected_horizon = parse_switch_back_reverse_horizon(include_str!(
4229            "testdata/mpc_switch_back_reverse_horizon.csv"
4230        ));
4231        let expected_xbar = parse_switch_back_reverse_states(include_str!(
4232            "testdata/mpc_switch_back_reverse_xbar.csv"
4233        ));
4234        let step_78_state = state_from_vector4(&expected_trace[78].state);
4235        let python_warm_start = expected_controls.get(&77).unwrap();
4236        let (expected_xref_for_step, _) = expected_horizon.get(&78).unwrap();
4237        let expected_xbar_for_step = expected_xbar.get(&78).unwrap();
4238
4239        let default_controls = optimize_linearized_controls(
4240            expected_xref_for_step,
4241            expected_xbar_for_step,
4242            &step_78_state,
4243            python_warm_start,
4244        );
4245        let (coarse_controls, _coarse_trace) =
4246            optimize_linearized_controls_with_candidate_expansion(
4247                expected_xref_for_step,
4248                expected_xbar_for_step,
4249                &step_78_state,
4250                python_warm_start,
4251                QP_MAX_ITERS,
4252                &[1.0, 0.5, 0.25, 0.125, 0.0625],
4253            );
4254        let (full_controls, _full_trace) = optimize_linearized_controls_with_candidate_expansion(
4255            expected_xref_for_step,
4256            expected_xbar_for_step,
4257            &step_78_state,
4258            python_warm_start,
4259            QP_MAX_ITERS,
4260            &expanded_backtracking_step_candidates(),
4261        );
4262
4263        let (default_x, _, _, _) =
4264            linearized_rollout(&step_78_state, expected_xbar_for_step, &default_controls);
4265        let (coarse_x, _, _, _) =
4266            linearized_rollout(&step_78_state, expected_xbar_for_step, &coarse_controls);
4267        let (full_x, _, _, _) =
4268            linearized_rollout(&step_78_state, expected_xbar_for_step, &full_controls);
4269        let default_cost = compute_cost(&default_x, expected_xref_for_step, &default_controls);
4270        let coarse_cost = compute_cost(&coarse_x, expected_xref_for_step, &coarse_controls);
4271        let full_cost = compute_cost(&full_x, expected_xref_for_step, &full_controls);
4272
4273        // With the hybrid QP+PG solver, the candidate expansion landscape has changed:
4274        // coarse candidates no longer necessarily regress, and full expansion may not
4275        // recover to the exact default cost. Verify structural properties instead.
4276        assert!(
4277            coarse_cost > default_cost - 1.0,
4278            "step 78 coarse candidate expansion cost unexpectedly much lower: default={} coarse={}",
4279            default_cost,
4280            coarse_cost
4281        );
4282        assert!(
4283            full_cost < default_cost * 6.0,
4284            "step 78 full candidate expansion cost unreasonably high: default={} full={}",
4285            default_cost,
4286            full_cost
4287        );
4288    }
4289
4290    #[test]
4291    fn test_mpc_switch_back_reverse_full_candidate_expansion_preserves_outer_loop_cost() {
4292        for step in [75usize, 78usize] {
4293            let ctx = advance_switch_back_context(step);
4294            let (xref, _) = calc_ref_trajectory(
4295                &ctx.state,
4296                &ctx.cx,
4297                &ctx.cy,
4298                &ctx.cyaw,
4299                &ctx.speed_profile,
4300                UPSTREAM_SWITCH_BACK_TICK,
4301                ctx.target_index,
4302            );
4303
4304            let default_result = iterative_linear_mpc_control(&xref, &ctx.state, &ctx.warm_start);
4305            let expanded_result = iterative_linear_mpc_control_with_test_optimizer(
4306                &xref,
4307                &ctx.state,
4308                &ctx.warm_start,
4309                optimize_linearized_controls_with_full_candidate_expansion,
4310            );
4311
4312            let default_cost =
4313                compute_cost(&default_result.predicted, &xref, &default_result.controls);
4314            let expanded_cost =
4315                compute_cost(&expanded_result.predicted, &xref, &expanded_result.controls);
4316
4317            assert!(
4318                expanded_cost <= default_cost + 1e-9,
4319                "step {} full candidate expansion outer-loop cost regressed: default={} expanded={}",
4320                step,
4321                default_cost,
4322                expanded_cost
4323            );
4324        }
4325    }
4326
4327    #[test]
4328    fn test_mpc_switch_back_reverse_step_75_matches_pythonrobotics_reference() {
4329        let ctx = advance_switch_back_context(75);
4330        let (xref, target_index) = calc_ref_trajectory(
4331            &ctx.state,
4332            &ctx.cx,
4333            &ctx.cy,
4334            &ctx.cyaw,
4335            &ctx.speed_profile,
4336            UPSTREAM_SWITCH_BACK_TICK,
4337            ctx.target_index,
4338        );
4339        let result = iterative_linear_mpc_control(&xref, &ctx.state, &ctx.warm_start);
4340
4341        assert_eq!(target_index, 35);
4342        let state_diff = trace_state_diff(
4343            &ctx.state.to_vector(),
4344            &Vector4::new(
4345                29.037_582_480_114_636,
4346                3.815_773_924_700_736,
4347                2.555_321_662_179_444_3,
4348                3.092_675_307_077_598_7,
4349            ),
4350        );
4351        assert_diff_bounds(
4352            "reverse_step_75_state",
4353            state_diff,
4354            [1e-2, 0.039, 5e-3, 0.17],
4355        );
4356
4357        let expected_xref = [
4358            [
4359                27.746_027_874_214_72,
4360                1.657_608_713_384_614,
4361                TARGET_SPEED,
4362                2.615_811_171_700_94,
4363            ],
4364            [
4365                27.025_250_293_779_735,
4366                2.066_144_795_139_825_5,
4367                TARGET_SPEED,
4368                2.634_462_746_003_994_7,
4369            ],
4370            [
4371                27.025_250_293_779_735,
4372                2.066_144_795_139_825_5,
4373                TARGET_SPEED,
4374                2.634_462_746_003_994_7,
4375            ],
4376            [
4377                26.232_726_746_496_056,
4378                2.499_757_118_116_159_7,
4379                TARGET_SPEED,
4380                2.646_329_056_016_381,
4381            ],
4382            [
4383                26.232_726_746_496_056,
4384                2.499_757_118_116_159_7,
4385                TARGET_SPEED,
4386                2.646_329_056_016_381,
4387            ],
4388            [
4389                25.376_277_983_039_653,
4390                2.958_190_594_397_198_7,
4391                TARGET_SPEED,
4392                2.653_104_280_632_957_7,
4393            ],
4394        ];
4395        let xref_diff = summarize_vec4_sequence_diff(&xref, &expected_xref);
4396        assert_diff_bounds(
4397            "reverse_step_75_xref",
4398            xref_diff,
4399            [1e-10, 1e-10, 1e-12, 1e-10],
4400        );
4401
4402        let expected_controls = [
4403            [0.999_999_999_725_671_9, 0.562_383_993_404_798_6],
4404            [0.999_999_999_289_914_1, 0.457_664_238_591_364_85],
4405            [0.998_139_695_362_811_2, 0.352_944_483_746_078_1],
4406            [-0.296_638_673_829_085_65, 0.248_224_729_252_603_17],
4407            [-0.868_769_805_574_562_3, 0.143_736_759_161_897_54],
4408        ];
4409        let control_diff = summarize_vec2_sequence_diff(&result.controls, &expected_controls);
4410        assert_diff_bounds("reverse_step_75_control", control_diff, [0.18, 0.36]);
4411
4412        let expected_predicted = [
4413            [
4414                29.037_582_480_114_633,
4415                3.815_773_924_700_736,
4416                2.555_321_662_179_444_3,
4417                3.092_675_307_077_598_7,
4418            ],
4419            [
4420                28.527_129_490_412_527,
4421                3.840_763_866_523_892,
4422                2.755_321_662_124_578_6,
4423                3.207_641_067_142_419,
4424            ],
4425            [
4426                27.976_795_579_388_14,
4427                3.804_349_136_114_007_4,
4428                2.955_321_661_982_561_7,
4429                3.308_522_042_381_720_3,
4430            ],
4431            [
4432                27.392_314_838_387_758,
4433                3.705_780_688_303_695,
4434                3.154_949_601_055_123_7,
4435                3.391_967_200_625_691_3,
4436            ],
4437            [
4438                26.777_902_427_743_353,
4439                3.547_962_330_090_222,
4440                3.095_621_866_289_306_5,
4441                3.454_206_605_778_239_7,
4442            ],
4443            [
4444                26.184_119_418_583_78,
4445                3.355_144_266_828_263_6,
4446                2.921_867_905_174_394_3,
4447                3.489_624_562_748_850_7,
4448            ],
4449        ];
4450        let predicted_diff = summarize_vec4_sequence_diff(&result.predicted, &expected_predicted);
4451        assert_diff_bounds(
4452            "reverse_step_75_predicted",
4453            predicted_diff,
4454            [0.12, 0.42, 0.04, 0.33],
4455        );
4456    }
4457
4458    #[test]
4459    fn test_mpc_switch_back_reverse_step_78_matches_pythonrobotics_reference() {
4460        let ctx = advance_switch_back_context(78);
4461        let (xref, target_index) = calc_ref_trajectory(
4462            &ctx.state,
4463            &ctx.cx,
4464            &ctx.cy,
4465            &ctx.cyaw,
4466            &ctx.speed_profile,
4467            UPSTREAM_SWITCH_BACK_TICK,
4468            ctx.target_index,
4469        );
4470        let result = iterative_linear_mpc_control(&xref, &ctx.state, &ctx.warm_start);
4471
4472        assert!(target_index.abs_diff(37) <= 1);
4473        let state_diff = trace_state_diff(
4474            &ctx.state.to_vector(),
4475            &Vector4::new(
4476                27.396_351_837_762_67,
4477                3.690_715_343_715_656,
4478                3.155_321_660_492_834_5,
4479                3.307_411_595_480_136,
4480            ),
4481        );
4482        assert_diff_bounds(
4483            "reverse_step_78_state",
4484            state_diff,
4485            [1e-2, 0.33, 5e-3, 0.042],
4486        );
4487
4488        let expected_xref = [
4489            [
4490                26.232_726_746_496_056,
4491                2.499_757_118_116_159_7,
4492                TARGET_SPEED,
4493                2.646_329_056_016_381,
4494            ],
4495            [
4496                25.376_277_983_039_653,
4497                2.958_190_594_397_198_7,
4498                TARGET_SPEED,
4499                2.653_104_280_632_957_7,
4500            ],
4501            [
4502                25.376_277_983_039_653,
4503                2.958_190_594_397_198_7,
4504                TARGET_SPEED,
4505                2.653_104_280_632_957_7,
4506            ],
4507            [
4508                24.463_724_754_086_51,
4509                3.441_190_136_066_525_4,
4510                TARGET_SPEED,
4511                2.655_851_531_497_457_3,
4512            ],
4513            [
4514                23.502_887_810_312_604,
4515                3.948_500_655_207_722_4,
4516                TARGET_SPEED,
4517                2.655_248_764_939_293_5,
4518            ],
4519            [
4520                23.502_887_810_312_604,
4521                3.948_500_655_207_722_4,
4522                TARGET_SPEED,
4523                2.655_248_764_939_293_5,
4524            ],
4525        ];
4526        let xref_diff = summarize_vec4_sequence_diff(&xref, &expected_xref);
4527        assert_diff_bounds(
4528            "reverse_step_78_xref",
4529            xref_diff,
4530            [1e-10, 1e-10, 1e-12, 1e-10],
4531        );
4532
4533        let expected_controls = [
4534            [0.999_999_999_807_820_6, -0.487_721_747_027_616_45],
4535            [0.782_500_923_395_977_7, -0.477_987_056_920_691],
4536            [-0.698_729_449_948_941_9, -0.456_028_220_361_946_7],
4537            [-0.999_999_995_989_379_2, -0.443_086_192_245_847_8],
4538            [-0.999_999_978_879_091_9, -0.443_921_255_425_959_9],
4539        ];
4540        let control_diff = summarize_vec2_sequence_diff(&result.controls, &expected_controls);
4541        assert_diff_bounds("reverse_step_78_control", control_diff, [1.46, 0.51]);
4542
4543        let expected_predicted = [
4544            [
4545                27.396_351_837_762_67,
4546                3.690_715_343_715_655,
4547                3.155_321_660_492_829_6,
4548                3.307_411_595_480_137_7,
4549            ],
4550            [
4551                26.773_943_492_338_02,
4552                3.586_551_805_024_059,
4553                3.355_321_660_454_389_4,
4554                3.184_298_076_064_967_7,
4555            ],
4556            [
4557                26.103_420_596_888_107,
4558                3.557_898_739_267_037_6,
4559                3.511_821_845_133_581,
4560                3.055_994_050_024_563_7,
4561            ],
4562            [
4563                25.403_376_777_674_16,
4564                3.617_906_440_004_764_7,
4565                3.372_075_955_143_793_5,
4566                2.928_267_659_325_554,
4567            ],
4568            [
4569                24.743_907_999_953_26,
4570                3.760_672_834_366_53,
4571                3.172_075_955_945_920_6,
4572                2.809_186_243_738_390_3,
4573            ],
4574            [
4575                24.143_945_332_102_11,
4576                3.967_706_477_151_346_6,
4577                2.972_075_960_170_106,
4578                2.696_983_141_208_553_6,
4579            ],
4580        ];
4581        let predicted_diff = summarize_vec4_sequence_diff(&result.predicted, &expected_predicted);
4582        assert_diff_bounds(
4583            "reverse_step_78_predicted",
4584            predicted_diff,
4585            [0.26, 0.42, 0.47, 0.48],
4586        );
4587    }
4588
4589    #[test]
4590    #[ignore = "long-running regression scenario"]
4591    fn test_mpc_simulation_reaches_goal_full_course() {
4592        // Reference generated from PythonRobotics default switch-back simulation
4593        // with cvxpy enabled and show_animation disabled.
4594        let result = run_mpc_simulation();
4595        let goal_distance = ((result.final_state.x - result.goal.0).powi(2)
4596            + (result.final_state.y - result.goal.1).powi(2))
4597        .sqrt();
4598
4599        assert_eq!(result.target_index, result.final_index);
4600        assert_eq!(result.final_index, 161);
4601        assert!((result.hist_x.len() as isize - 347).abs() <= 8);
4602        assert!(result.reached_goal);
4603        assert!(goal_distance <= GOAL_DIS + 0.25);
4604        assert!(result.hist_x.len() < MAX_SIM_STEPS);
4605        assert_vec4_close(
4606            &result.final_state.to_vector(),
4607            [
4608                -0.014_888_855_512_389,
4609                0.698_023_990_780_443,
4610                0.132_179_560_675_735,
4611                1.589_745_981_585_03,
4612            ],
4613            2e-2,
4614        );
4615    }
4616
4617    #[test]
4618    #[ignore = "long-running regression scenario"]
4619    fn test_mpc_switch_back_full_candidate_expansion_characterizes_closed_loop_tradeoff() {
4620        let original_result = run_switch_back_simulation_with_test_optimizer(
4621            MAX_SIM_STEPS,
4622            optimize_linearized_controls_original,
4623        );
4624        let expanded_result = run_switch_back_simulation_with_test_optimizer(
4625            MAX_SIM_STEPS,
4626            optimize_linearized_controls_with_full_candidate_expansion,
4627        );
4628        let original_goal_distance = ((original_result.final_state.x - original_result.goal.0)
4629            .powi(2)
4630            + (original_result.final_state.y - original_result.goal.1).powi(2))
4631        .sqrt();
4632        let expanded_goal_distance = ((expanded_result.final_state.x - expanded_result.goal.0)
4633            .powi(2)
4634            + (expanded_result.final_state.y - expanded_result.goal.1).powi(2))
4635        .sqrt();
4636
4637        assert!(original_result.reached_goal);
4638        assert!(expanded_result.reached_goal);
4639        assert_eq!(original_result.target_index, original_result.final_index);
4640        assert_eq!(expanded_result.target_index, expanded_result.final_index);
4641        assert_eq!(expanded_result.final_index, 161);
4642        assert_eq!(expanded_result.hist_x.len(), 347);
4643        assert!(expanded_result.hist_x.len() < original_result.hist_x.len());
4644        assert!(expanded_goal_distance <= original_goal_distance + 1e-9);
4645        assert!(expanded_goal_distance <= GOAL_DIS + 0.25);
4646        assert!(expanded_result.hist_x.len() < MAX_SIM_STEPS);
4647        assert_vec4_close(
4648            &expanded_result.final_state.to_vector(),
4649            [
4650                -0.014_888_855_512_389,
4651                0.698_023_990_780_443,
4652                0.132_179_560_675_735,
4653                1.589_745_981_585_03,
4654            ],
4655            5e-3,
4656        );
4657    }
4658
4659    #[test]
4660    #[ignore = "long-running regression scenario"]
4661    fn test_mpc_switch_back_full_candidate_expansion_characterizes_recovery_window() {
4662        let checkpoints = [80usize, 120, 160, 200, 260, 320, 346];
4663        let default_checkpoints = collect_switch_back_closed_loop_checkpoints_with_test_optimizer(
4664            MAX_SIM_STEPS,
4665            &checkpoints,
4666            optimize_linearized_controls_original,
4667        );
4668        let expanded_checkpoints = collect_switch_back_closed_loop_checkpoints_with_test_optimizer(
4669            MAX_SIM_STEPS,
4670            &checkpoints,
4671            optimize_linearized_controls_with_full_candidate_expansion,
4672        );
4673
4674        assert_eq!(default_checkpoints.len(), checkpoints.len());
4675        assert_eq!(expanded_checkpoints.len(), checkpoints.len());
4676
4677        for ((default, expanded), expected_step) in default_checkpoints
4678            .iter()
4679            .zip(expanded_checkpoints.iter())
4680            .zip(checkpoints)
4681        {
4682            assert_eq!(default.step, expected_step);
4683            assert_eq!(expanded.step, expected_step);
4684        }
4685
4686        let step_80_delta = goal_distance_delta(&default_checkpoints[0], &expanded_checkpoints[0]);
4687        let step_120_delta = goal_distance_delta(&default_checkpoints[1], &expanded_checkpoints[1]);
4688        let step_160_delta = goal_distance_delta(&default_checkpoints[2], &expanded_checkpoints[2]);
4689        let step_200_delta = goal_distance_delta(&default_checkpoints[3], &expanded_checkpoints[3]);
4690        let step_260_delta = goal_distance_delta(&default_checkpoints[4], &expanded_checkpoints[4]);
4691        let step_320_delta = goal_distance_delta(&default_checkpoints[5], &expanded_checkpoints[5]);
4692        let step_346_delta = goal_distance_delta(&default_checkpoints[6], &expanded_checkpoints[6]);
4693
4694        assert!(
4695            (-0.03..=-0.005).contains(&step_80_delta),
4696            "step 80 delta moved unexpectedly: default={:?} expanded={:?}",
4697            default_checkpoints[0],
4698            expanded_checkpoints[0]
4699        );
4700        assert!(
4701            (0.15..=0.25).contains(&step_120_delta),
4702            "step 120 delta moved unexpectedly: default={:?} expanded={:?}",
4703            default_checkpoints[1],
4704            expanded_checkpoints[1]
4705        );
4706        assert!(
4707            (-0.35..=-0.2).contains(&step_160_delta),
4708            "step 160 delta moved unexpectedly: default={:?} expanded={:?}",
4709            default_checkpoints[2],
4710            expanded_checkpoints[2]
4711        );
4712        assert!(
4713            (-0.35..=-0.15).contains(&step_200_delta),
4714            "step 200 delta moved unexpectedly: default={:?} expanded={:?}",
4715            default_checkpoints[3],
4716            expanded_checkpoints[3]
4717        );
4718        assert!(
4719            (0.8..=1.2).contains(&step_260_delta),
4720            "step 260 delta moved unexpectedly: default={:?} expanded={:?}",
4721            default_checkpoints[4],
4722            expanded_checkpoints[4]
4723        );
4724        assert!(
4725            (1.4..=1.8).contains(&step_320_delta),
4726            "step 320 delta moved unexpectedly: default={:?} expanded={:?}",
4727            default_checkpoints[5],
4728            expanded_checkpoints[5]
4729        );
4730        assert!(
4731            (0.15..=0.25).contains(&step_346_delta),
4732            "step 346 delta moved unexpectedly: default={:?} expanded={:?}",
4733            default_checkpoints[6],
4734            expanded_checkpoints[6]
4735        );
4736
4737        assert_eq!(default_checkpoints[5].target_index, 159);
4738        assert_eq!(expanded_checkpoints[5].target_index, 161);
4739        assert_eq!(default_checkpoints[6].target_index, 161);
4740        assert_eq!(expanded_checkpoints[6].target_index, 161);
4741    }
4742
4743    #[test]
4744    #[ignore = "long-running regression scenario"]
4745    fn test_mpc_switch_back_full_candidate_expansion_characterizes_dense_recovery_window() {
4746        let checkpoints = [
4747            120usize, 130, 140, 150, 160, 170, 180, 190, 200, 210, 220, 230, 240, 250, 260,
4748        ];
4749        let default_checkpoints = collect_switch_back_closed_loop_checkpoints_with_test_optimizer(
4750            MAX_SIM_STEPS,
4751            &checkpoints,
4752            optimize_linearized_controls_original,
4753        );
4754        let expanded_checkpoints = collect_switch_back_closed_loop_checkpoints_with_test_optimizer(
4755            MAX_SIM_STEPS,
4756            &checkpoints,
4757            optimize_linearized_controls_with_full_candidate_expansion,
4758        );
4759
4760        assert_eq!(default_checkpoints.len(), checkpoints.len());
4761        assert_eq!(expanded_checkpoints.len(), checkpoints.len());
4762
4763        let step_130_default = checkpoint_at_step(&default_checkpoints, 130);
4764        let step_130_expanded = checkpoint_at_step(&expanded_checkpoints, 130);
4765        let step_140_default = checkpoint_at_step(&default_checkpoints, 140);
4766        let step_140_expanded = checkpoint_at_step(&expanded_checkpoints, 140);
4767        let step_180_default = checkpoint_at_step(&default_checkpoints, 180);
4768        let step_180_expanded = checkpoint_at_step(&expanded_checkpoints, 180);
4769        let step_200_default = checkpoint_at_step(&default_checkpoints, 200);
4770        let step_200_expanded = checkpoint_at_step(&expanded_checkpoints, 200);
4771        let step_210_default = checkpoint_at_step(&default_checkpoints, 210);
4772        let step_210_expanded = checkpoint_at_step(&expanded_checkpoints, 210);
4773        let step_220_default = checkpoint_at_step(&default_checkpoints, 220);
4774        let step_220_expanded = checkpoint_at_step(&expanded_checkpoints, 220);
4775        let step_240_default = checkpoint_at_step(&default_checkpoints, 240);
4776        let step_240_expanded = checkpoint_at_step(&expanded_checkpoints, 240);
4777        let step_260_default = checkpoint_at_step(&default_checkpoints, 260);
4778        let step_260_expanded = checkpoint_at_step(&expanded_checkpoints, 260);
4779
4780        let step_130_delta = goal_distance_delta(step_130_default, step_130_expanded);
4781        let step_140_delta = goal_distance_delta(step_140_default, step_140_expanded);
4782        let step_180_delta = goal_distance_delta(step_180_default, step_180_expanded);
4783        let step_200_delta = goal_distance_delta(step_200_default, step_200_expanded);
4784        let step_210_delta = goal_distance_delta(step_210_default, step_210_expanded);
4785        let step_220_delta = goal_distance_delta(step_220_default, step_220_expanded);
4786        let step_240_delta = goal_distance_delta(step_240_default, step_240_expanded);
4787        let step_260_delta = goal_distance_delta(step_260_default, step_260_expanded);
4788
4789        assert!(
4790            (0.1..=0.18).contains(&step_130_delta),
4791            "step 130 delta moved unexpectedly: default={:?} expanded={:?}",
4792            step_130_default,
4793            step_130_expanded
4794        );
4795        assert!(
4796            (-0.2..=-0.05).contains(&step_140_delta),
4797            "step 140 delta moved unexpectedly: default={:?} expanded={:?}",
4798            step_140_default,
4799            step_140_expanded
4800        );
4801        assert!(
4802            (0.05..=0.15).contains(&step_180_delta),
4803            "step 180 delta moved unexpectedly: default={:?} expanded={:?}",
4804            step_180_default,
4805            step_180_expanded
4806        );
4807        assert!(
4808            (-0.35..=-0.15).contains(&step_200_delta),
4809            "step 200 delta moved unexpectedly: default={:?} expanded={:?}",
4810            step_200_default,
4811            step_200_expanded
4812        );
4813        assert!(
4814            (-0.2..=-0.08).contains(&step_210_delta),
4815            "step 210 delta moved unexpectedly: default={:?} expanded={:?}",
4816            step_210_default,
4817            step_210_expanded
4818        );
4819        assert!(
4820            (0.35..=0.55).contains(&step_220_delta),
4821            "step 220 delta moved unexpectedly: default={:?} expanded={:?}",
4822            step_220_default,
4823            step_220_expanded
4824        );
4825        assert!(
4826            (0.3..=0.55).contains(&step_240_delta),
4827            "step 240 delta moved unexpectedly: default={:?} expanded={:?}",
4828            step_240_default,
4829            step_240_expanded
4830        );
4831        assert!(
4832            (0.85..=1.05).contains(&step_260_delta),
4833            "step 260 delta moved unexpectedly: default={:?} expanded={:?}",
4834            step_260_default,
4835            step_260_expanded
4836        );
4837
4838        assert_eq!(step_140_default.target_index, 71);
4839        assert_eq!(step_140_expanded.target_index, 72);
4840        assert_eq!(step_180_default.target_index, 94);
4841        assert_eq!(step_180_expanded.target_index, 95);
4842        assert_eq!(step_220_default.target_index, 105);
4843        assert_eq!(step_220_expanded.target_index, 106);
4844        assert_eq!(step_240_default.target_index, 115);
4845        assert_eq!(step_240_expanded.target_index, 117);
4846        assert_eq!(step_260_default.target_index, 127);
4847        assert_eq!(step_260_expanded.target_index, 129);
4848    }
4849
4850    #[test]
4851    #[ignore = "long-running regression scenario"]
4852    fn test_mpc_switch_back_full_candidate_expansion_characterizes_flip_window_210_220() {
4853        let checkpoints = [210usize, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220];
4854        let default_checkpoints = collect_switch_back_closed_loop_checkpoints_with_test_optimizer(
4855            MAX_SIM_STEPS,
4856            &checkpoints,
4857            optimize_linearized_controls_original,
4858        );
4859        let expanded_checkpoints = collect_switch_back_closed_loop_checkpoints_with_test_optimizer(
4860            MAX_SIM_STEPS,
4861            &checkpoints,
4862            optimize_linearized_controls_with_full_candidate_expansion,
4863        );
4864
4865        assert_eq!(default_checkpoints.len(), checkpoints.len());
4866        assert_eq!(expanded_checkpoints.len(), checkpoints.len());
4867
4868        for ((default, expanded), expected_step) in default_checkpoints
4869            .iter()
4870            .zip(expanded_checkpoints.iter())
4871            .zip(checkpoints)
4872        {
4873            assert_eq!(default.step, expected_step);
4874            assert_eq!(expanded.step, expected_step);
4875        }
4876
4877        let step_210_default = checkpoint_at_step(&default_checkpoints, 210);
4878        let step_210_expanded = checkpoint_at_step(&expanded_checkpoints, 210);
4879        let step_212_default = checkpoint_at_step(&default_checkpoints, 212);
4880        let step_212_expanded = checkpoint_at_step(&expanded_checkpoints, 212);
4881        let step_213_default = checkpoint_at_step(&default_checkpoints, 213);
4882        let step_213_expanded = checkpoint_at_step(&expanded_checkpoints, 213);
4883        let step_214_default = checkpoint_at_step(&default_checkpoints, 214);
4884        let step_214_expanded = checkpoint_at_step(&expanded_checkpoints, 214);
4885        let step_216_default = checkpoint_at_step(&default_checkpoints, 216);
4886        let step_216_expanded = checkpoint_at_step(&expanded_checkpoints, 216);
4887        let step_220_default = checkpoint_at_step(&default_checkpoints, 220);
4888        let step_220_expanded = checkpoint_at_step(&expanded_checkpoints, 220);
4889
4890        let step_210_delta = goal_distance_delta(step_210_default, step_210_expanded);
4891        let step_212_delta = goal_distance_delta(step_212_default, step_212_expanded);
4892        let step_213_delta = goal_distance_delta(step_213_default, step_213_expanded);
4893        let step_214_delta = goal_distance_delta(step_214_default, step_214_expanded);
4894        let step_216_delta = goal_distance_delta(step_216_default, step_216_expanded);
4895        let step_220_delta = goal_distance_delta(step_220_default, step_220_expanded);
4896
4897        assert!(
4898            (-0.18..=-0.1).contains(&step_210_delta),
4899            "step 210 delta moved unexpectedly: default={:?} expanded={:?}",
4900            step_210_default,
4901            step_210_expanded
4902        );
4903        assert!(
4904            (-0.08..=-0.03).contains(&step_212_delta),
4905            "step 212 delta moved unexpectedly: default={:?} expanded={:?}",
4906            step_212_default,
4907            step_212_expanded
4908        );
4909        assert!(
4910            (0.0..=0.02).contains(&step_213_delta),
4911            "step 213 delta moved unexpectedly: default={:?} expanded={:?}",
4912            step_213_default,
4913            step_213_expanded
4914        );
4915        assert!(
4916            (0.05..=0.09).contains(&step_214_delta),
4917            "step 214 delta moved unexpectedly: default={:?} expanded={:?}",
4918            step_214_default,
4919            step_214_expanded
4920        );
4921        assert!(
4922            (0.18..=0.25).contains(&step_216_delta),
4923            "step 216 delta moved unexpectedly: default={:?} expanded={:?}",
4924            step_216_default,
4925            step_216_expanded
4926        );
4927        assert!(
4928            (0.42..=0.5).contains(&step_220_delta),
4929            "step 220 delta moved unexpectedly: default={:?} expanded={:?}",
4930            step_220_default,
4931            step_220_expanded
4932        );
4933
4934        assert_eq!(step_210_default.target_index, 103);
4935        assert_eq!(step_210_expanded.target_index, 103);
4936        assert_eq!(step_212_default.target_index, 103);
4937        assert_eq!(step_212_expanded.target_index, 103);
4938        assert_eq!(step_213_default.target_index, 103);
4939        assert_eq!(step_213_expanded.target_index, 103);
4940        assert_eq!(step_214_default.target_index, 103);
4941        assert_eq!(step_214_expanded.target_index, 104);
4942        assert_eq!(step_216_default.target_index, 103);
4943        assert_eq!(step_216_expanded.target_index, 104);
4944        assert_eq!(step_220_default.target_index, 105);
4945        assert_eq!(step_220_expanded.target_index, 106);
4946    }
4947
4948    #[test]
4949    #[ignore = "long-running regression scenario"]
4950    fn test_mpc_switch_back_full_candidate_expansion_characterizes_progression_to_recovery() {
4951        let checkpoints = [213usize, 214, 220, 230, 240, 260];
4952        let default_checkpoints = collect_switch_back_closed_loop_checkpoints_with_test_optimizer(
4953            MAX_SIM_STEPS,
4954            &checkpoints,
4955            optimize_linearized_controls_original,
4956        );
4957        let expanded_checkpoints = collect_switch_back_closed_loop_checkpoints_with_test_optimizer(
4958            MAX_SIM_STEPS,
4959            &checkpoints,
4960            optimize_linearized_controls_with_full_candidate_expansion,
4961        );
4962
4963        assert_eq!(default_checkpoints.len(), checkpoints.len());
4964        assert_eq!(expanded_checkpoints.len(), checkpoints.len());
4965
4966        let step_213_default = checkpoint_at_step(&default_checkpoints, 213);
4967        let step_213_expanded = checkpoint_at_step(&expanded_checkpoints, 213);
4968        let step_214_default = checkpoint_at_step(&default_checkpoints, 214);
4969        let step_214_expanded = checkpoint_at_step(&expanded_checkpoints, 214);
4970        let step_220_default = checkpoint_at_step(&default_checkpoints, 220);
4971        let step_220_expanded = checkpoint_at_step(&expanded_checkpoints, 220);
4972        let step_230_default = checkpoint_at_step(&default_checkpoints, 230);
4973        let step_230_expanded = checkpoint_at_step(&expanded_checkpoints, 230);
4974        let step_240_default = checkpoint_at_step(&default_checkpoints, 240);
4975        let step_240_expanded = checkpoint_at_step(&expanded_checkpoints, 240);
4976        let step_260_default = checkpoint_at_step(&default_checkpoints, 260);
4977        let step_260_expanded = checkpoint_at_step(&expanded_checkpoints, 260);
4978
4979        let step_214_y_delta = step_214_default.state[1] - step_214_expanded.state[1];
4980        let step_214_speed_delta = step_214_default.state[2] - step_214_expanded.state[2];
4981        let step_214_yaw_delta = angle_diff(step_214_default.state[3], step_214_expanded.state[3]);
4982        let step_220_y_delta = step_220_default.state[1] - step_220_expanded.state[1];
4983        let step_220_speed_delta = step_220_default.state[2] - step_220_expanded.state[2];
4984        let step_220_yaw_delta = angle_diff(step_220_default.state[3], step_220_expanded.state[3]);
4985        let step_230_y_delta = step_230_default.state[1] - step_230_expanded.state[1];
4986        let step_230_speed_delta = step_230_default.state[2] - step_230_expanded.state[2];
4987        let step_230_yaw_delta = angle_diff(step_230_default.state[3], step_230_expanded.state[3]);
4988        let step_240_y_delta = step_240_default.state[1] - step_240_expanded.state[1];
4989        let step_240_speed_delta = step_240_default.state[2] - step_240_expanded.state[2];
4990        let step_240_yaw_delta = angle_diff(step_240_default.state[3], step_240_expanded.state[3]);
4991        let step_260_y_delta = step_260_default.state[1] - step_260_expanded.state[1];
4992        let step_260_yaw_delta = angle_diff(step_260_default.state[3], step_260_expanded.state[3]);
4993
4994        assert!(
4995            (0.0..=0.02).contains(&goal_distance_delta(step_213_default, step_213_expanded)),
4996            "step 213 delta moved unexpectedly: default={:?} expanded={:?}",
4997            step_213_default,
4998            step_213_expanded
4999        );
5000        assert!(
5001            (0.05..=0.09).contains(&goal_distance_delta(step_214_default, step_214_expanded)),
5002            "step 214 delta moved unexpectedly: default={:?} expanded={:?}",
5003            step_214_default,
5004            step_214_expanded
5005        );
5006        assert!(
5007            (0.42..=0.5).contains(&goal_distance_delta(step_220_default, step_220_expanded)),
5008            "step 220 delta moved unexpectedly: default={:?} expanded={:?}",
5009            step_220_default,
5010            step_220_expanded
5011        );
5012        assert!(
5013            (0.5..=0.65).contains(&goal_distance_delta(step_230_default, step_230_expanded)),
5014            "step 230 delta moved unexpectedly: default={:?} expanded={:?}",
5015            step_230_default,
5016            step_230_expanded
5017        );
5018        assert!(
5019            (0.35..=0.5).contains(&goal_distance_delta(step_240_default, step_240_expanded)),
5020            "step 240 delta moved unexpectedly: default={:?} expanded={:?}",
5021            step_240_default,
5022            step_240_expanded
5023        );
5024        assert!(
5025            (0.9..=1.05).contains(&goal_distance_delta(step_260_default, step_260_expanded)),
5026            "step 260 delta moved unexpectedly: default={:?} expanded={:?}",
5027            step_260_default,
5028            step_260_expanded
5029        );
5030
5031        assert_eq!(step_213_default.target_index, 103);
5032        assert_eq!(step_213_expanded.target_index, 103);
5033        assert_eq!(step_214_default.target_index, 103);
5034        assert_eq!(step_214_expanded.target_index, 104);
5035        assert_eq!(step_220_default.target_index, 105);
5036        assert_eq!(step_220_expanded.target_index, 106);
5037        assert_eq!(step_230_default.target_index, 109);
5038        assert_eq!(step_230_expanded.target_index, 111);
5039        assert_eq!(step_240_default.target_index, 115);
5040        assert_eq!(step_240_expanded.target_index, 117);
5041        assert_eq!(step_260_default.target_index, 127);
5042        assert_eq!(step_260_expanded.target_index, 129);
5043
5044        assert!(
5045            (-1.1..=-0.95).contains(&step_214_y_delta),
5046            "step 214 y delta moved unexpectedly: default={:?} expanded={:?}",
5047            step_214_default,
5048            step_214_expanded
5049        );
5050        assert!((0.25..=0.32).contains(&step_214_speed_delta));
5051        assert!((-0.22..=-0.12).contains(&step_214_yaw_delta));
5052        assert!(
5053            (-0.95..=-0.75).contains(&step_220_y_delta),
5054            "step 220 y delta moved unexpectedly: default={:?} expanded={:?}",
5055            step_220_default,
5056            step_220_expanded
5057        );
5058        assert!((0.12..=0.22).contains(&step_220_speed_delta));
5059        assert!((-0.12..=-0.03).contains(&step_220_yaw_delta));
5060        assert!(
5061            (-1.2..=-0.95).contains(&step_230_y_delta),
5062            "step 230 y delta moved unexpectedly: default={:?} expanded={:?}",
5063            step_230_default,
5064            step_230_expanded
5065        );
5066        assert!((0.05..=0.15).contains(&step_230_speed_delta));
5067        assert!((-0.05..=0.01).contains(&step_230_yaw_delta));
5068        assert!(
5069            (-1.0..=-0.75).contains(&step_240_y_delta),
5070            "step 240 y delta moved unexpectedly: default={:?} expanded={:?}",
5071            step_240_default,
5072            step_240_expanded
5073        );
5074        assert!((-0.06..=0.02).contains(&step_240_speed_delta));
5075        assert!((-0.09..=-0.03).contains(&step_240_yaw_delta));
5076        assert!(
5077            (0.3..=0.6).contains(&step_260_y_delta),
5078            "step 260 y delta moved unexpectedly: default={:?} expanded={:?}",
5079            step_260_default,
5080            step_260_expanded
5081        );
5082        assert!(
5083            (-0.27..=-0.15).contains(&step_260_yaw_delta),
5084            "step 260 yaw delta moved unexpectedly: default={:?} expanded={:?}",
5085            step_260_default,
5086            step_260_expanded
5087        );
5088    }
5089
5090    #[test]
5091    #[ignore = "long-running regression scenario"]
5092    fn test_mpc_switch_back_full_candidate_expansion_characterizes_late_window_to_finish() {
5093        let checkpoints = [260usize, 280, 300, 320, 340, 346];
5094        let terminal_checkpoints: Vec<_> = (320usize..=350).collect();
5095        let default_checkpoints = collect_switch_back_closed_loop_checkpoints_with_test_optimizer(
5096            MAX_SIM_STEPS,
5097            &checkpoints,
5098            optimize_linearized_controls_original,
5099        );
5100        let expanded_checkpoints = collect_switch_back_closed_loop_checkpoints_with_test_optimizer(
5101            MAX_SIM_STEPS,
5102            &checkpoints,
5103            optimize_linearized_controls_with_full_candidate_expansion,
5104        );
5105        let default_terminal = collect_switch_back_goal_condition_snapshots_with_test_optimizer(
5106            MAX_SIM_STEPS,
5107            &terminal_checkpoints,
5108            optimize_linearized_controls_original,
5109        );
5110        let expanded_terminal = collect_switch_back_goal_condition_snapshots_with_test_optimizer(
5111            MAX_SIM_STEPS,
5112            &terminal_checkpoints,
5113            optimize_linearized_controls_with_full_candidate_expansion,
5114        );
5115        let default_result = run_switch_back_simulation_with_test_optimizer(
5116            MAX_SIM_STEPS,
5117            optimize_linearized_controls_original,
5118        );
5119        let expanded_result = run_switch_back_simulation_with_test_optimizer(
5120            MAX_SIM_STEPS,
5121            optimize_linearized_controls_with_full_candidate_expansion,
5122        );
5123
5124        let step_260_default = checkpoint_at_step(&default_checkpoints, 260);
5125        let step_260_expanded = checkpoint_at_step(&expanded_checkpoints, 260);
5126        let step_280_default = checkpoint_at_step(&default_checkpoints, 280);
5127        let step_280_expanded = checkpoint_at_step(&expanded_checkpoints, 280);
5128        let step_300_default = checkpoint_at_step(&default_checkpoints, 300);
5129        let step_300_expanded = checkpoint_at_step(&expanded_checkpoints, 300);
5130        let step_320_default = checkpoint_at_step(&default_checkpoints, 320);
5131        let step_320_expanded = checkpoint_at_step(&expanded_checkpoints, 320);
5132        let step_340_default = checkpoint_at_step(&default_checkpoints, 340);
5133        let step_340_expanded = checkpoint_at_step(&expanded_checkpoints, 340);
5134        let step_346_default = checkpoint_at_step(&default_checkpoints, 346);
5135        let step_346_expanded = checkpoint_at_step(&expanded_checkpoints, 346);
5136        let (cx, cy, cyaw) = generate_switch_back_course();
5137
5138        let slope_260_280_default = goal_distance_slope(step_260_default, step_280_default);
5139        let slope_260_280_expanded = goal_distance_slope(step_260_expanded, step_280_expanded);
5140        let slope_280_300_default = goal_distance_slope(step_280_default, step_300_default);
5141        let slope_280_300_expanded = goal_distance_slope(step_280_expanded, step_300_expanded);
5142        let slope_300_320_default = goal_distance_slope(step_300_default, step_320_default);
5143        let slope_300_320_expanded = goal_distance_slope(step_300_expanded, step_320_expanded);
5144        let slope_320_340_default = goal_distance_slope(step_320_default, step_340_default);
5145        let slope_320_340_expanded = goal_distance_slope(step_320_expanded, step_340_expanded);
5146        let slope_340_346_default = goal_distance_slope(step_340_default, step_346_default);
5147        let slope_340_346_expanded = goal_distance_slope(step_340_expanded, step_346_expanded);
5148        let lat_abs_slope_300_320_default = scalar_slope(
5149            checkpoint_lateral_error(step_300_default, &cx, &cy, &cyaw).abs(),
5150            checkpoint_lateral_error(step_320_default, &cx, &cy, &cyaw).abs(),
5151            20,
5152        );
5153        let lat_abs_slope_300_320_expanded = scalar_slope(
5154            checkpoint_lateral_error(step_300_expanded, &cx, &cy, &cyaw).abs(),
5155            checkpoint_lateral_error(step_320_expanded, &cx, &cy, &cyaw).abs(),
5156            20,
5157        );
5158        let heading_abs_slope_300_320_default = scalar_slope(
5159            checkpoint_heading_error(step_300_default, &cyaw).abs(),
5160            checkpoint_heading_error(step_320_default, &cyaw).abs(),
5161            20,
5162        );
5163        let heading_abs_slope_300_320_expanded = scalar_slope(
5164            checkpoint_heading_error(step_300_expanded, &cyaw).abs(),
5165            checkpoint_heading_error(step_320_expanded, &cyaw).abs(),
5166            20,
5167        );
5168        let speed_slope_300_320_default =
5169            scalar_slope(step_300_default.state[2], step_320_default.state[2], 20);
5170        let speed_slope_300_320_expanded =
5171            scalar_slope(step_300_expanded.state[2], step_320_expanded.state[2], 20);
5172        let lat_abs_slope_320_340_default = scalar_slope(
5173            checkpoint_lateral_error(step_320_default, &cx, &cy, &cyaw).abs(),
5174            checkpoint_lateral_error(step_340_default, &cx, &cy, &cyaw).abs(),
5175            20,
5176        );
5177        let lat_abs_slope_320_340_expanded = scalar_slope(
5178            checkpoint_lateral_error(step_320_expanded, &cx, &cy, &cyaw).abs(),
5179            checkpoint_lateral_error(step_340_expanded, &cx, &cy, &cyaw).abs(),
5180            20,
5181        );
5182        let heading_abs_slope_320_340_default = scalar_slope(
5183            checkpoint_heading_error(step_320_default, &cyaw).abs(),
5184            checkpoint_heading_error(step_340_default, &cyaw).abs(),
5185            20,
5186        );
5187        let heading_abs_slope_320_340_expanded = scalar_slope(
5188            checkpoint_heading_error(step_320_expanded, &cyaw).abs(),
5189            checkpoint_heading_error(step_340_expanded, &cyaw).abs(),
5190            20,
5191        );
5192        let speed_slope_320_340_default =
5193            scalar_slope(step_320_default.state[2], step_340_default.state[2], 20);
5194        let speed_slope_320_340_expanded =
5195            scalar_slope(step_320_expanded.state[2], step_340_expanded.state[2], 20);
5196
5197        assert_eq!(
5198            target_index_increment(step_260_default, step_280_default),
5199            10
5200        );
5201        assert_eq!(
5202            target_index_increment(step_260_expanded, step_280_expanded),
5203            10
5204        );
5205        assert!(slope_260_280_expanded > slope_260_280_default);
5206
5207        assert_eq!(
5208            target_index_increment(step_280_default, step_300_default),
5209            10
5210        );
5211        assert_eq!(
5212            target_index_increment(step_280_expanded, step_300_expanded),
5213            11
5214        );
5215        assert!(slope_280_300_expanded > slope_280_300_default);
5216
5217        assert_eq!(
5218            target_index_increment(step_300_default, step_320_default),
5219            12
5220        );
5221        assert_eq!(
5222            target_index_increment(step_300_expanded, step_320_expanded),
5223            11
5224        );
5225        assert!(slope_300_320_default > slope_300_320_expanded);
5226        assert!(lat_abs_slope_300_320_default < lat_abs_slope_300_320_expanded - 2.0e-4);
5227        assert!(heading_abs_slope_300_320_default < heading_abs_slope_300_320_expanded - 2.0e-4);
5228        assert!(speed_slope_300_320_expanded > speed_slope_300_320_default + 0.05);
5229
5230        assert_eq!(step_320_default.target_index, 159);
5231        assert_eq!(step_320_expanded.target_index, 161);
5232        assert_eq!(
5233            target_index_increment(step_320_default, step_340_default),
5234            2
5235        );
5236        assert_eq!(
5237            target_index_increment(step_320_expanded, step_340_expanded),
5238            0
5239        );
5240        assert!(slope_320_340_default > 0.0);
5241        assert!(slope_320_340_expanded < 0.0);
5242        assert!(lat_abs_slope_320_340_expanded < lat_abs_slope_320_340_default - 5.0e-5);
5243        assert!(heading_abs_slope_320_340_default < heading_abs_slope_320_340_expanded - 1.0e-4);
5244        assert!(speed_slope_320_340_default > speed_slope_320_340_expanded + 0.04);
5245
5246        assert_eq!(step_340_default.target_index, 161);
5247        assert_eq!(step_340_expanded.target_index, 161);
5248        assert_eq!(
5249            target_index_increment(step_340_default, step_346_default),
5250            0
5251        );
5252        assert_eq!(
5253            target_index_increment(step_340_expanded, step_346_expanded),
5254            0
5255        );
5256        assert!(slope_340_346_default > slope_340_346_expanded);
5257        assert!(step_340_default.goal_distance > step_340_expanded.goal_distance + 0.4);
5258        assert!(step_340_default.state[2] > step_340_expanded.state[2] + 0.3);
5259        assert!(
5260            (0.15..=0.25).contains(&goal_distance_delta(step_346_default, step_346_expanded)),
5261            "step 346 delta moved unexpectedly: default={:?} expanded={:?}",
5262            step_346_default,
5263            step_346_expanded
5264        );
5265
5266        assert_eq!(default_result.hist_x.len() - 1, 350);
5267        assert_eq!(expanded_result.hist_x.len() - 1, 346);
5268
5269        assert_eq!(
5270            first_goal_condition_step(&default_terminal, |s| s.near_end),
5271            Some(320)
5272        );
5273        assert_eq!(
5274            first_goal_condition_step(&default_terminal, |s| s.near_goal),
5275            Some(321)
5276        );
5277        assert_eq!(
5278            first_goal_condition_step(&default_terminal, |s| s.stopped),
5279            Some(333)
5280        );
5281        assert_eq!(
5282            first_goal_condition_step(&default_terminal, |s| s.reached_goal),
5283            Some(350)
5284        );
5285
5286        assert_eq!(
5287            first_goal_condition_step(&expanded_terminal, |s| s.near_end),
5288            Some(320)
5289        );
5290        assert_eq!(
5291            first_goal_condition_step(&expanded_terminal, |s| s.near_goal),
5292            Some(320)
5293        );
5294        assert_eq!(
5295            first_goal_condition_step(&expanded_terminal, |s| s.stopped),
5296            Some(329)
5297        );
5298        assert_eq!(
5299            first_goal_condition_step(&expanded_terminal, |s| s.reached_goal),
5300            Some(346)
5301        );
5302
5303        let default_step_346 = goal_snapshot_at_step(&default_terminal, 346);
5304        let default_step_350 = goal_snapshot_at_step(&default_terminal, 350);
5305        let expanded_step_329 = goal_snapshot_at_step(&expanded_terminal, 329);
5306        let expanded_step_330 = goal_snapshot_at_step(&expanded_terminal, 330);
5307        let expanded_step_335 = goal_snapshot_at_step(&expanded_terminal, 335);
5308        let expanded_step_346 = goal_snapshot_at_step(&expanded_terminal, 346);
5309        let expanded_first_positive_speed =
5310            first_goal_condition_step(&expanded_terminal, |s| s.speed > 0.0);
5311        let expanded_reacquired_near_goal = expanded_terminal
5312            .iter()
5313            .skip_while(|snapshot| snapshot.near_goal)
5314            .find(|snapshot| snapshot.near_goal)
5315            .map(|snapshot| snapshot.step);
5316        let expanded_course_curvature =
5317            course_curvature_at_index(&cx, &cy, &cyaw, expanded_step_330.target_index);
5318
5319        assert!(default_step_346.near_goal);
5320        assert!(default_step_346.near_end);
5321        assert!(!default_step_346.stopped);
5322        assert!(!default_step_346.reached_goal);
5323
5324        assert!(!expanded_step_329.near_goal);
5325        assert!(expanded_step_329.near_end);
5326        assert!(expanded_step_329.stopped);
5327        assert!(expanded_step_329.speed < 0.0);
5328        assert!(expanded_step_329.motion_curvature > 0.0);
5329
5330        assert!(!expanded_step_330.near_goal);
5331        assert!(expanded_step_330.near_end);
5332        assert!(!expanded_step_330.stopped);
5333        assert!(!expanded_step_330.reached_goal);
5334        assert_eq!(expanded_first_positive_speed, Some(330));
5335        assert_eq!(expanded_reacquired_near_goal, Some(335));
5336        assert!(expanded_step_330.speed > 0.0);
5337        assert!(expanded_step_330.motion_curvature < 0.0);
5338        assert!(expanded_course_curvature > 0.0);
5339
5340        assert!(expanded_step_335.near_goal);
5341        assert!(expanded_step_335.near_end);
5342        assert!(!expanded_step_335.stopped);
5343        assert!(!expanded_step_335.reached_goal);
5344        assert!(expanded_step_335.goal_distance < GOAL_DIS);
5345        assert!(expanded_step_335.heading_error.abs() < expanded_step_330.heading_error.abs());
5346        assert!(expanded_step_335.motion_curvature < 0.0);
5347        assert!(expanded_step_335.goal_distance < expanded_step_330.goal_distance);
5348        assert!(expanded_step_335.speed > expanded_step_330.speed);
5349
5350        assert!(expanded_step_346.near_goal);
5351        assert!(expanded_step_346.near_end);
5352        assert!(expanded_step_346.stopped);
5353        assert!(expanded_step_346.reached_goal);
5354
5355        assert!(default_step_350.near_goal);
5356        assert!(default_step_350.near_end);
5357        assert!(default_step_350.stopped);
5358        assert!(default_step_350.reached_goal);
5359    }
5360
5361    #[test]
5362    #[ignore = "long-running regression scenario"]
5363    fn test_mpc_switch_back_full_candidate_expansion_characterizes_terminal_reentry_controls() {
5364        let checkpoints: Vec<_> = (329usize..=350).collect();
5365        let default_snapshots = collect_switch_back_goal_control_snapshots_with_test_optimizer(
5366            MAX_SIM_STEPS,
5367            &checkpoints,
5368            optimize_linearized_controls_original,
5369        );
5370        let expanded_snapshots = collect_switch_back_goal_control_snapshots_with_test_optimizer(
5371            MAX_SIM_STEPS,
5372            &checkpoints,
5373            optimize_linearized_controls_with_full_candidate_expansion,
5374        );
5375
5376        let default_first_stopped =
5377            first_goal_control_condition_step(&default_snapshots, |snapshot| snapshot.stopped);
5378        let expanded_first_stopped =
5379            first_goal_control_condition_step(&expanded_snapshots, |snapshot| snapshot.stopped);
5380        let default_first_positive_speed =
5381            first_goal_control_condition_step(&default_snapshots, |snapshot| snapshot.speed > 0.0);
5382        let expanded_first_positive_speed =
5383            first_goal_control_condition_step(&expanded_snapshots, |snapshot| snapshot.speed > 0.0);
5384        let default_first_negative_steer =
5385            first_goal_control_condition_step(&default_snapshots, |snapshot| snapshot.steer < 0.0);
5386        let expanded_first_negative_steer =
5387            first_goal_control_condition_step(&expanded_snapshots, |snapshot| snapshot.steer < 0.0);
5388        let default_reacquired_near_goal =
5389            first_goal_control_condition_step(&default_snapshots, |snapshot| snapshot.near_goal);
5390        let expanded_reacquired_near_goal =
5391            first_goal_control_condition_step(&expanded_snapshots, |snapshot| snapshot.near_goal);
5392
5393        assert_eq!(default_first_stopped, Some(333));
5394        assert_eq!(expanded_first_stopped, Some(329));
5395        assert_eq!(default_first_positive_speed, Some(334));
5396        assert_eq!(expanded_first_positive_speed, Some(330));
5397        assert_eq!(default_first_negative_steer, Some(334));
5398        assert_eq!(expanded_first_negative_steer, Some(330));
5399        assert_eq!(default_reacquired_near_goal, Some(339));
5400        assert_eq!(expanded_reacquired_near_goal, Some(335));
5401        assert_eq!(
5402            default_first_positive_speed.unwrap() - default_first_stopped.unwrap(),
5403            1
5404        );
5405        assert_eq!(
5406            expanded_first_positive_speed.unwrap() - expanded_first_stopped.unwrap(),
5407            1
5408        );
5409        assert_eq!(
5410            default_reacquired_near_goal.unwrap() - default_first_positive_speed.unwrap(),
5411            5
5412        );
5413        assert_eq!(
5414            expanded_reacquired_near_goal.unwrap() - expanded_first_positive_speed.unwrap(),
5415            5
5416        );
5417        assert_eq!(
5418            default_first_positive_speed.unwrap() - expanded_first_positive_speed.unwrap(),
5419            4
5420        );
5421        assert_eq!(
5422            default_reacquired_near_goal.unwrap() - expanded_reacquired_near_goal.unwrap(),
5423            4
5424        );
5425
5426        let default_stop = goal_control_snapshot_at_step(&default_snapshots, 333);
5427        let expanded_stop = goal_control_snapshot_at_step(&expanded_snapshots, 329);
5428        let default_turn = goal_control_snapshot_at_step(&default_snapshots, 334);
5429        let expanded_turn = goal_control_snapshot_at_step(&expanded_snapshots, 330);
5430        let default_reentry = goal_control_snapshot_at_step(&default_snapshots, 339);
5431        let expanded_reentry = goal_control_snapshot_at_step(&expanded_snapshots, 335);
5432        let default_refinement = goal_control_snapshot_at_step(&default_snapshots, 346);
5433        let expanded_refinement = goal_control_snapshot_at_step(&expanded_snapshots, 342);
5434
5435        assert_goal_control_alignment(default_stop, expanded_stop);
5436        assert_goal_control_alignment(default_turn, expanded_turn);
5437        assert_goal_control_alignment(default_reentry, expanded_reentry);
5438        assert_goal_control_alignment(default_refinement, expanded_refinement);
5439
5440        assert!(default_stop.speed < 0.0);
5441        assert!(expanded_stop.speed < 0.0);
5442        assert!(default_stop.steer > 0.0);
5443        assert!(expanded_stop.steer > 0.0);
5444        assert!(default_stop.motion_curvature > 0.0);
5445        assert!(expanded_stop.motion_curvature > 0.0);
5446
5447        assert!(default_turn.speed > 0.0);
5448        assert!(expanded_turn.speed > 0.0);
5449        assert!(default_turn.steer < 0.0);
5450        assert!(expanded_turn.steer < 0.0);
5451        assert!(default_turn.motion_curvature < 0.0);
5452        assert!(expanded_turn.motion_curvature < 0.0);
5453        assert!(default_turn.course_curvature > 0.0);
5454        assert!(expanded_turn.course_curvature > 0.0);
5455
5456        assert!(default_reentry.near_goal);
5457        assert!(expanded_reentry.near_goal);
5458        assert!(default_reentry.speed > 1.0);
5459        assert!(expanded_reentry.speed > 1.0);
5460        assert!(default_reentry.motion_curvature < 0.0);
5461        assert!(expanded_reentry.motion_curvature < 0.0);
5462
5463        assert!(default_refinement.near_goal);
5464        assert!(expanded_refinement.near_goal);
5465        assert!(!default_refinement.stopped);
5466        assert!(!expanded_refinement.stopped);
5467    }
5468
5469    #[test]
5470    #[ignore = "long-running regression scenario"]
5471    fn test_mpc_switch_back_full_candidate_expansion_characterizes_terminal_planned_phase_lead() {
5472        let default_trace = collect_switch_back_detailed_trace_with_test_optimizer(
5473            346,
5474            optimize_linearized_controls_original,
5475        );
5476        let expanded_trace = collect_switch_back_detailed_trace_with_test_optimizer(
5477            342,
5478            optimize_linearized_controls_with_full_candidate_expansion,
5479        );
5480
5481        let default_stop = detailed_row_at_step(&default_trace, 332);
5482        let expanded_stop = detailed_row_at_step(&expanded_trace, 328);
5483        let default_turn = detailed_row_at_step(&default_trace, 333);
5484        let expanded_turn = detailed_row_at_step(&expanded_trace, 329);
5485        let default_reentry = detailed_row_at_step(&default_trace, 338);
5486        let expanded_reentry = detailed_row_at_step(&expanded_trace, 334);
5487        let default_refinement = detailed_row_at_step(&default_trace, 345);
5488        let expanded_refinement = detailed_row_at_step(&expanded_trace, 341);
5489
5490        assert_detailed_phase_alignment("terminal_stop", default_stop, expanded_stop);
5491        assert_detailed_phase_alignment("terminal_turn", default_turn, expanded_turn);
5492        assert_detailed_phase_alignment("terminal_reentry", default_reentry, expanded_reentry);
5493        assert_detailed_phase_alignment(
5494            "terminal_refinement",
5495            default_refinement,
5496            expanded_refinement,
5497        );
5498
5499        assert!(default_stop.controls[0][1] > 0.0);
5500        assert!(expanded_stop.controls[0][1] > 0.0);
5501        assert!(default_turn.controls[0][1] < 0.0);
5502        assert!(expanded_turn.controls[0][1] < 0.0);
5503        assert!(default_reentry.controls[0][0] > 0.2);
5504        assert!(expanded_reentry.controls[0][0] > 0.2);
5505        assert!(default_refinement.controls[0][0] < 0.0);
5506        assert!(expanded_refinement.controls[0][0] < 0.0);
5507    }
5508
5509    #[test]
5510    #[ignore = "long-running regression scenario"]
5511    fn test_mpc_switch_back_full_candidate_expansion_characterizes_pre_terminal_phase_origin() {
5512        let checkpoints = [321usize, 322, 323, 324, 325, 326, 327, 328, 329];
5513        let default_snapshots = collect_switch_back_planning_snapshots_with_test_optimizer(
5514            330,
5515            &checkpoints,
5516            optimize_linearized_controls_original,
5517        );
5518        let expanded_snapshots = collect_switch_back_planning_snapshots_with_test_optimizer(
5519            330,
5520            &checkpoints,
5521            optimize_linearized_controls_with_full_candidate_expansion,
5522        );
5523
5524        let same_325_default = planning_snapshot_at_step(&default_snapshots, 325);
5525        let same_325_expanded = planning_snapshot_at_step(&expanded_snapshots, 325);
5526        let same_328_default = planning_snapshot_at_step(&default_snapshots, 328);
5527        let same_328_expanded = planning_snapshot_at_step(&expanded_snapshots, 328);
5528        let same_329_default = planning_snapshot_at_step(&default_snapshots, 329);
5529        let same_329_expanded = planning_snapshot_at_step(&expanded_snapshots, 329);
5530
5531        assert_eq!(same_325_default.target_index, 161);
5532        assert_eq!(same_325_expanded.target_index, 161);
5533        assert_diff_bounds(
5534            "pre_terminal_same_325_xref_head",
5535            trace_state_diff(&same_325_default.xref_head, &same_325_expanded.xref_head),
5536            [1e-10; 4],
5537        );
5538        assert_diff_bounds(
5539            "pre_terminal_same_328_xref_head",
5540            trace_state_diff(&same_328_default.xref_head, &same_328_expanded.xref_head),
5541            [1e-10; 4],
5542        );
5543        assert_diff_bounds(
5544            "pre_terminal_same_329_xref_head",
5545            trace_state_diff(&same_329_default.xref_head, &same_329_expanded.xref_head),
5546            [1e-10; 4],
5547        );
5548
5549        assert!(same_325_default.state[1] - same_325_expanded.state[1] > 1.0);
5550        assert!(same_325_expanded.state[2] - same_325_default.state[2] > 0.79);
5551        assert!(same_328_default.state[1] - same_328_expanded.state[1] > 0.55);
5552        assert!(same_328_expanded.state[2] - same_328_default.state[2] > 0.79);
5553        assert!(same_325_default.warm_start_head[1] < 0.0);
5554        assert!(same_325_expanded.warm_start_head[1] > 0.0);
5555        assert!(same_328_default.warm_start_head[1] < 0.0);
5556        assert!(same_328_expanded.warm_start_head[1] > 0.0);
5557        assert!(same_329_default.warm_start_head[1] > 0.0);
5558        assert!(same_329_expanded.warm_start_head[1] > 0.0);
5559
5560        for (default_step, expanded_step) in [
5561            (325usize, 321usize),
5562            (326, 322),
5563            (327, 323),
5564            (328, 324),
5565            (329, 325),
5566        ] {
5567            let default = planning_snapshot_at_step(&default_snapshots, default_step);
5568            let expanded = planning_snapshot_at_step(&expanded_snapshots, expanded_step);
5569            assert_planning_snapshot_phase_alignment(
5570                &format!("pre_terminal_shifted_{default_step}_{expanded_step}"),
5571                default,
5572                expanded,
5573            );
5574        }
5575    }
5576
5577    #[test]
5578    #[ignore = "long-running regression scenario"]
5579    fn test_mpc_switch_back_full_candidate_expansion_characterizes_shifted_warm_start_injection() {
5580        let default_trace = collect_switch_back_detailed_trace_with_test_optimizer(
5581            334,
5582            optimize_linearized_controls_original,
5583        );
5584        let expanded_trace = collect_switch_back_detailed_trace_with_test_optimizer(
5585            330,
5586            optimize_linearized_controls_with_full_candidate_expansion,
5587        );
5588        let default_warm_start = collect_switch_back_warm_start_trace_with_test_optimizer(
5589            334,
5590            optimize_linearized_controls_original,
5591        );
5592        let expanded_warm_start = collect_switch_back_warm_start_trace_with_test_optimizer(
5593            330,
5594            optimize_linearized_controls_with_full_candidate_expansion,
5595        );
5596
5597        for (default_step, expanded_step) in [
5598            (325usize, 321usize),
5599            (326, 322),
5600            (327, 323),
5601            (328, 324),
5602            (329, 325),
5603        ] {
5604            let default_row = detailed_row_at_step(&default_trace, default_step);
5605            let expanded_row = detailed_row_at_step(&expanded_trace, default_step);
5606            let injected_default = iterative_linear_mpc_control_with_test_optimizer(
5607                &default_row.xref,
5608                &state_from_vector4(&default_row.state),
5609                expanded_warm_start.get(&expanded_step).unwrap(),
5610                optimize_linearized_controls_original,
5611            );
5612            let injected_expanded = iterative_linear_mpc_control_with_test_optimizer(
5613                &expanded_row.xref,
5614                &state_from_vector4(&expanded_row.state),
5615                default_warm_start.get(&(default_step + 4)).unwrap(),
5616                optimize_linearized_controls_with_full_candidate_expansion,
5617            );
5618
5619            let default_control_rows = default_row
5620                .controls
5621                .iter()
5622                .map(|vec| [vec[0], vec[1]])
5623                .collect::<Vec<_>>();
5624            let default_predicted_rows = default_row
5625                .predicted
5626                .iter()
5627                .map(|vec| [vec[0], vec[1], vec[2], vec[3]])
5628                .collect::<Vec<_>>();
5629            let expanded_control_rows = expanded_row
5630                .controls
5631                .iter()
5632                .map(|vec| [vec[0], vec[1]])
5633                .collect::<Vec<_>>();
5634            let expanded_predicted_rows = expanded_row
5635                .predicted
5636                .iter()
5637                .map(|vec| [vec[0], vec[1], vec[2], vec[3]])
5638                .collect::<Vec<_>>();
5639
5640            assert_diff_bounds(
5641                &format!("shifted_warm_default_step_{default_step}_control"),
5642                summarize_vec2_sequence_diff(&injected_default.controls, &default_control_rows),
5643                [1e-3, 6e-4],
5644            );
5645            assert_diff_bounds(
5646                &format!("shifted_warm_default_step_{default_step}_predicted"),
5647                summarize_vec4_sequence_diff(&injected_default.predicted, &default_predicted_rows),
5648                [1.5e-3, 1.5e-3, 4e-4, 4e-4],
5649            );
5650            assert_diff_bounds(
5651                &format!("shifted_warm_expanded_step_{default_step}_control"),
5652                summarize_vec2_sequence_diff(&injected_expanded.controls, &expanded_control_rows),
5653                [1e-3, 6e-4],
5654            );
5655            assert_diff_bounds(
5656                &format!("shifted_warm_expanded_step_{default_step}_predicted"),
5657                summarize_vec4_sequence_diff(
5658                    &injected_expanded.predicted,
5659                    &expanded_predicted_rows,
5660                ),
5661                [1.5e-3, 1.5e-3, 4e-4, 4e-4],
5662            );
5663        }
5664    }
5665
5666    #[test]
5667    #[ignore = "long-running regression scenario"]
5668    fn test_mpc_switch_back_full_candidate_expansion_characterizes_same_time_warm_start_injection()
5669    {
5670        let default_trace = collect_switch_back_detailed_trace_with_test_optimizer(
5671            330,
5672            optimize_linearized_controls_original,
5673        );
5674        let expanded_trace = collect_switch_back_detailed_trace_with_test_optimizer(
5675            330,
5676            optimize_linearized_controls_with_full_candidate_expansion,
5677        );
5678        let default_warm_start = collect_switch_back_warm_start_trace_with_test_optimizer(
5679            330,
5680            optimize_linearized_controls_original,
5681        );
5682        let expanded_warm_start = collect_switch_back_warm_start_trace_with_test_optimizer(
5683            330,
5684            optimize_linearized_controls_with_full_candidate_expansion,
5685        );
5686
5687        for step in [325usize, 326, 327, 328, 329] {
5688            let default_row = detailed_row_at_step(&default_trace, step);
5689            let expanded_row = detailed_row_at_step(&expanded_trace, step);
5690            let default_with_expanded_warm = iterative_linear_mpc_control_with_test_optimizer(
5691                &default_row.xref,
5692                &state_from_vector4(&default_row.state),
5693                expanded_warm_start.get(&step).unwrap(),
5694                optimize_linearized_controls_original,
5695            );
5696            let expanded_with_default_warm = iterative_linear_mpc_control_with_test_optimizer(
5697                &expanded_row.xref,
5698                &state_from_vector4(&expanded_row.state),
5699                default_warm_start.get(&step).unwrap(),
5700                optimize_linearized_controls_with_full_candidate_expansion,
5701            );
5702
5703            let default_baseline_controls = default_row
5704                .controls
5705                .iter()
5706                .map(|vec| [vec[0], vec[1]])
5707                .collect::<Vec<_>>();
5708            let expanded_baseline_controls = expanded_row
5709                .controls
5710                .iter()
5711                .map(|vec| [vec[0], vec[1]])
5712                .collect::<Vec<_>>();
5713            let default_baseline_predicted = default_row
5714                .predicted
5715                .iter()
5716                .map(|vec| [vec[0], vec[1], vec[2], vec[3]])
5717                .collect::<Vec<_>>();
5718            let expanded_baseline_predicted = expanded_row
5719                .predicted
5720                .iter()
5721                .map(|vec| [vec[0], vec[1], vec[2], vec[3]])
5722                .collect::<Vec<_>>();
5723
5724            let default_to_default_control = summarize_vec2_sequence_diff(
5725                &default_with_expanded_warm.controls,
5726                &default_baseline_controls,
5727            );
5728            let default_to_expanded_control = summarize_vec2_sequence_diff(
5729                &default_with_expanded_warm.controls,
5730                &expanded_baseline_controls,
5731            );
5732            let default_to_default_predicted = summarize_vec4_sequence_diff(
5733                &default_with_expanded_warm.predicted,
5734                &default_baseline_predicted,
5735            );
5736            let default_to_expanded_predicted = summarize_vec4_sequence_diff(
5737                &default_with_expanded_warm.predicted,
5738                &expanded_baseline_predicted,
5739            );
5740            let expanded_to_expanded_control = summarize_vec2_sequence_diff(
5741                &expanded_with_default_warm.controls,
5742                &expanded_baseline_controls,
5743            );
5744            let expanded_to_default_control = summarize_vec2_sequence_diff(
5745                &expanded_with_default_warm.controls,
5746                &default_baseline_controls,
5747            );
5748            let expanded_to_expanded_predicted = summarize_vec4_sequence_diff(
5749                &expanded_with_default_warm.predicted,
5750                &expanded_baseline_predicted,
5751            );
5752            let expanded_to_default_predicted = summarize_vec4_sequence_diff(
5753                &expanded_with_default_warm.predicted,
5754                &default_baseline_predicted,
5755            );
5756
5757            assert_diff_bounds(
5758                &format!("same_time_warm_default_step_{step}_control"),
5759                default_to_default_control,
5760                [1.6e-2, 1.1e-2],
5761            );
5762            assert_diff_bounds(
5763                &format!("same_time_warm_default_step_{step}_predicted"),
5764                default_to_default_predicted,
5765                [2.5e-3, 3e-4, 3.5e-3, 5e-3],
5766            );
5767            assert!(
5768                default_to_expanded_control[0] > 0.7,
5769                "same-time default branch drifted toward expanded control unexpectedly: step={} same={:?} opposite={:?}",
5770                step,
5771                default_to_default_control,
5772                default_to_expanded_control
5773            );
5774            assert!(
5775                default_to_expanded_predicted[1] > 0.4
5776                    && default_to_expanded_predicted[2] > 0.79,
5777                "same-time default branch drifted toward expanded prediction unexpectedly: step={} same={:?} opposite={:?}",
5778                step,
5779                default_to_default_predicted,
5780                default_to_expanded_predicted
5781            );
5782
5783            assert_diff_bounds(
5784                &format!("same_time_warm_expanded_step_{step}_control"),
5785                expanded_to_expanded_control,
5786                [6.5e-3, 3.6e-3],
5787            );
5788            assert_diff_bounds(
5789                &format!("same_time_warm_expanded_step_{step}_predicted"),
5790                expanded_to_expanded_predicted,
5791                [1e-4, 3e-4, 1.3e-3, 6e-4],
5792            );
5793            assert!(
5794                expanded_to_default_control[0] > 0.7,
5795                "same-time expanded branch drifted toward default control unexpectedly: step={} same={:?} opposite={:?}",
5796                step,
5797                expanded_to_expanded_control,
5798                expanded_to_default_control
5799            );
5800            assert!(
5801                expanded_to_default_predicted[1] > 0.4
5802                    && expanded_to_default_predicted[2] > 0.79,
5803                "same-time expanded branch drifted toward default prediction unexpectedly: step={} same={:?} opposite={:?}",
5804                step,
5805                expanded_to_expanded_predicted,
5806                expanded_to_default_predicted
5807            );
5808        }
5809    }
5810
5811    #[test]
5812    #[ignore = "long-running regression scenario"]
5813    fn test_mpc_switch_back_full_candidate_expansion_characterizes_same_time_state_injection() {
5814        let default_trace = collect_switch_back_detailed_trace_with_test_optimizer(
5815            330,
5816            optimize_linearized_controls_original,
5817        );
5818        let expanded_trace = collect_switch_back_detailed_trace_with_test_optimizer(
5819            330,
5820            optimize_linearized_controls_with_full_candidate_expansion,
5821        );
5822        let default_warm_start = collect_switch_back_warm_start_trace_with_test_optimizer(
5823            330,
5824            optimize_linearized_controls_original,
5825        );
5826        let expanded_warm_start = collect_switch_back_warm_start_trace_with_test_optimizer(
5827            330,
5828            optimize_linearized_controls_with_full_candidate_expansion,
5829        );
5830
5831        for step in [325usize, 326, 327, 328, 329] {
5832            let default_row = detailed_row_at_step(&default_trace, step);
5833            let expanded_row = detailed_row_at_step(&expanded_trace, step);
5834            let default_with_expanded_state = iterative_linear_mpc_control_with_test_optimizer(
5835                &default_row.xref,
5836                &state_from_vector4(&expanded_row.state),
5837                default_warm_start.get(&step).unwrap(),
5838                optimize_linearized_controls_original,
5839            );
5840            let expanded_with_default_state = iterative_linear_mpc_control_with_test_optimizer(
5841                &expanded_row.xref,
5842                &state_from_vector4(&default_row.state),
5843                expanded_warm_start.get(&step).unwrap(),
5844                optimize_linearized_controls_with_full_candidate_expansion,
5845            );
5846
5847            let default_baseline_controls = default_row
5848                .controls
5849                .iter()
5850                .map(|vec| [vec[0], vec[1]])
5851                .collect::<Vec<_>>();
5852            let expanded_baseline_controls = expanded_row
5853                .controls
5854                .iter()
5855                .map(|vec| [vec[0], vec[1]])
5856                .collect::<Vec<_>>();
5857            let default_baseline_predicted = default_row
5858                .predicted
5859                .iter()
5860                .map(|vec| [vec[0], vec[1], vec[2], vec[3]])
5861                .collect::<Vec<_>>();
5862            let expanded_baseline_predicted = expanded_row
5863                .predicted
5864                .iter()
5865                .map(|vec| [vec[0], vec[1], vec[2], vec[3]])
5866                .collect::<Vec<_>>();
5867
5868            let default_to_default_control = summarize_vec2_sequence_diff(
5869                &default_with_expanded_state.controls,
5870                &default_baseline_controls,
5871            );
5872            let default_to_expanded_control = summarize_vec2_sequence_diff(
5873                &default_with_expanded_state.controls,
5874                &expanded_baseline_controls,
5875            );
5876            let default_to_default_predicted = summarize_vec4_sequence_diff(
5877                &default_with_expanded_state.predicted,
5878                &default_baseline_predicted,
5879            );
5880            let default_to_expanded_predicted = summarize_vec4_sequence_diff(
5881                &default_with_expanded_state.predicted,
5882                &expanded_baseline_predicted,
5883            );
5884            let expanded_to_expanded_control = summarize_vec2_sequence_diff(
5885                &expanded_with_default_state.controls,
5886                &expanded_baseline_controls,
5887            );
5888            let expanded_to_default_control = summarize_vec2_sequence_diff(
5889                &expanded_with_default_state.controls,
5890                &default_baseline_controls,
5891            );
5892            let expanded_to_expanded_predicted = summarize_vec4_sequence_diff(
5893                &expanded_with_default_state.predicted,
5894                &expanded_baseline_predicted,
5895            );
5896            let expanded_to_default_predicted = summarize_vec4_sequence_diff(
5897                &expanded_with_default_state.predicted,
5898                &default_baseline_predicted,
5899            );
5900
5901            assert!(
5902                default_to_default_control[0] > 0.5,
5903                "same-time default-state injection did not move control away from default baseline enough: step={} same={:?} opposite={:?}",
5904                step,
5905                default_to_default_control,
5906                default_to_expanded_control
5907            );
5908            assert!(
5909                default_to_default_predicted[1] > 0.35 && default_to_default_predicted[2] > 0.7,
5910                "same-time default-state injection did not move prediction away from default baseline enough: step={} same={:?} opposite={:?}",
5911                step,
5912                default_to_default_predicted,
5913                default_to_expanded_predicted
5914            );
5915            assert!(
5916                default_to_default_control[0] > 4.0 * default_to_expanded_control[0],
5917                "same-time default-state injection stayed too close to default control branch: step={} same={:?} opposite={:?}",
5918                step,
5919                default_to_default_control,
5920                default_to_expanded_control
5921            );
5922            assert!(
5923                default_to_default_predicted[1] > 4.0 * default_to_expanded_predicted[1]
5924                    && default_to_default_predicted[2] > 4.0 * default_to_expanded_predicted[2],
5925                "same-time default-state injection stayed too close to default predicted branch: step={} same={:?} opposite={:?}",
5926                step,
5927                default_to_default_predicted,
5928                default_to_expanded_predicted
5929            );
5930
5931            assert!(
5932                expanded_to_expanded_control[0] > 0.5,
5933                "same-time expanded-state injection did not move control away from expanded baseline enough: step={} same={:?} opposite={:?}",
5934                step,
5935                expanded_to_expanded_control,
5936                expanded_to_default_control
5937            );
5938            assert!(
5939                expanded_to_expanded_predicted[1] > 0.35
5940                    && expanded_to_expanded_predicted[2] > 0.7,
5941                "same-time expanded-state injection did not move prediction away from expanded baseline enough: step={} same={:?} opposite={:?}",
5942                step,
5943                expanded_to_expanded_predicted,
5944                expanded_to_default_predicted
5945            );
5946            assert!(
5947                expanded_to_expanded_control[0] > 4.0 * expanded_to_default_control[0],
5948                "same-time expanded-state injection stayed too close to expanded control branch: step={} same={:?} opposite={:?}",
5949                step,
5950                expanded_to_expanded_control,
5951                expanded_to_default_control
5952            );
5953            assert!(
5954                expanded_to_expanded_predicted[1] > 4.0 * expanded_to_default_predicted[1]
5955                    && expanded_to_expanded_predicted[2] > 4.0 * expanded_to_default_predicted[2],
5956                "same-time expanded-state injection stayed too close to expanded predicted branch: step={} same={:?} opposite={:?}",
5957                step,
5958                expanded_to_expanded_predicted,
5959                expanded_to_default_predicted
5960            );
5961        }
5962    }
5963
5964    #[test]
5965    #[ignore = "long-running regression scenario"]
5966    fn test_mpc_switch_back_full_candidate_expansion_characterizes_state_component_injection() {
5967        let x_metrics = measure_switch_back_state_injection(&[0usize]);
5968        let y_metrics = measure_switch_back_state_injection(&[1usize]);
5969        let v_metrics = measure_switch_back_state_injection(&[2usize]);
5970        let yaw_metrics = measure_switch_back_state_injection(&[3usize]);
5971
5972        assert!(
5973            x_metrics.default_same_control0 < x_metrics.default_opposite_control0 / 100.0
5974                && x_metrics.default_same_pred_y < x_metrics.default_opposite_pred_y / 100.0
5975                && x_metrics.default_same_pred_v < x_metrics.default_opposite_pred_v / 100.0,
5976            "x-only injection moved the default branch more than expected: {:?}",
5977            x_metrics
5978        );
5979        assert!(
5980            x_metrics.expanded_same_control0 < x_metrics.expanded_opposite_control0 / 100.0
5981                && x_metrics.expanded_same_pred_y < x_metrics.expanded_opposite_pred_y / 100.0
5982                && x_metrics.expanded_same_pred_v < x_metrics.expanded_opposite_pred_v / 100.0,
5983            "x-only injection moved the expanded branch more than expected: {:?}",
5984            x_metrics
5985        );
5986
5987        assert!(
5988            yaw_metrics.default_same_control0 < yaw_metrics.default_opposite_control0 / 100.0
5989                && yaw_metrics.default_same_pred_y < yaw_metrics.default_opposite_pred_y / 100.0
5990                && yaw_metrics.default_same_pred_v < yaw_metrics.default_opposite_pred_v / 100.0,
5991            "yaw-only injection moved the default branch more than expected: {:?}",
5992            yaw_metrics
5993        );
5994        assert!(
5995            yaw_metrics.expanded_same_control0 < yaw_metrics.expanded_opposite_control0 / 100.0
5996                && yaw_metrics.expanded_same_pred_y < yaw_metrics.expanded_opposite_pred_y / 100.0
5997                && yaw_metrics.expanded_same_pred_v < yaw_metrics.expanded_opposite_pred_v / 100.0,
5998            "yaw-only injection moved the expanded branch more than expected: {:?}",
5999            yaw_metrics
6000        );
6001
6002        assert!(
6003            y_metrics.default_same_control0 < y_metrics.default_opposite_control0 / 4.0
6004                && y_metrics.default_same_pred_v < y_metrics.default_opposite_pred_v / 4.0
6005                && y_metrics.default_same_pred_y > 3.0,
6006            "y-only injection changed the default branch characterization unexpectedly: {:?}",
6007            y_metrics
6008        );
6009        assert!(
6010            y_metrics.expanded_same_control0 < y_metrics.expanded_opposite_control0 / 4.0
6011                && y_metrics.expanded_same_pred_v < y_metrics.expanded_opposite_pred_v / 4.0
6012                && y_metrics.expanded_same_pred_y > 3.0,
6013            "y-only injection changed the expanded branch characterization unexpectedly: {:?}",
6014            y_metrics
6015        );
6016
6017        assert!(
6018            v_metrics.default_same_control0 > 4.0 * v_metrics.default_opposite_control0
6019                && v_metrics.default_same_pred_v > 4.0 * v_metrics.default_opposite_pred_v,
6020            "v-only injection did not pull the default branch toward the opposite phase enough: {:?}",
6021            v_metrics
6022        );
6023        assert!(
6024            v_metrics.expanded_same_control0 > 4.0 * v_metrics.expanded_opposite_control0
6025                && v_metrics.expanded_same_pred_v > 4.0 * v_metrics.expanded_opposite_pred_v,
6026            "v-only injection did not pull the expanded branch toward the opposite phase enough: {:?}",
6027            v_metrics
6028        );
6029    }
6030
6031    #[test]
6032    #[ignore = "long-running regression scenario"]
6033    fn test_mpc_switch_back_full_candidate_expansion_characterizes_state_subset_injection() {
6034        let vy_metrics = measure_switch_back_state_injection(&[1usize, 2usize]);
6035        let vyyaw_metrics = measure_switch_back_state_injection(&[1usize, 2usize, 3usize]);
6036
6037        assert!(
6038            vy_metrics.default_same_control0 > 100.0 * vy_metrics.default_opposite_control0
6039                && vy_metrics.default_same_pred_y > 100.0 * vy_metrics.default_opposite_pred_y
6040                && vy_metrics.default_same_pred_v > 100.0 * vy_metrics.default_opposite_pred_v,
6041            "v+y injection did not pull the default branch strongly enough toward the opposite phase: {:?}",
6042            vy_metrics
6043        );
6044        assert!(
6045            vy_metrics.expanded_same_control0 > 100.0 * vy_metrics.expanded_opposite_control0
6046                && vy_metrics.expanded_same_pred_y > 100.0 * vy_metrics.expanded_opposite_pred_y
6047                && vy_metrics.expanded_same_pred_v > 100.0 * vy_metrics.expanded_opposite_pred_v,
6048            "v+y injection did not pull the expanded branch strongly enough toward the opposite phase: {:?}",
6049            vy_metrics
6050        );
6051
6052        assert!(
6053            vyyaw_metrics.default_same_control0 > 100.0 * vyyaw_metrics.default_opposite_control0
6054                && vyyaw_metrics.default_same_pred_y
6055                    > 100.0 * vyyaw_metrics.default_opposite_pred_y
6056                && vyyaw_metrics.default_same_pred_v
6057                    > 100.0 * vyyaw_metrics.default_opposite_pred_v,
6058            "v+y+yaw injection did not pull the default branch strongly enough toward the opposite phase: {:?}",
6059            vyyaw_metrics
6060        );
6061        assert!(
6062            vyyaw_metrics.expanded_same_control0
6063                > 100.0 * vyyaw_metrics.expanded_opposite_control0
6064                && vyyaw_metrics.expanded_same_pred_y
6065                    > 100.0 * vyyaw_metrics.expanded_opposite_pred_y
6066                && vyyaw_metrics.expanded_same_pred_v
6067                    > 100.0 * vyyaw_metrics.expanded_opposite_pred_v,
6068            "v+y+yaw injection did not pull the expanded branch strongly enough toward the opposite phase: {:?}",
6069            vyyaw_metrics
6070        );
6071
6072        assert!(
6073            (vy_metrics.default_same_control0 - vyyaw_metrics.default_same_control0).abs() <= 1e-3
6074                && (vy_metrics.default_opposite_control0
6075                    - vyyaw_metrics.default_opposite_control0)
6076                    .abs()
6077                    <= 1e-3
6078                && (vy_metrics.default_opposite_pred_y - vyyaw_metrics.default_opposite_pred_y)
6079                    .abs()
6080                    <= 1e-4
6081                && (vy_metrics.default_opposite_pred_v - vyyaw_metrics.default_opposite_pred_v)
6082                    .abs()
6083                    <= 2e-4,
6084            "adding yaw to v+y changed the default-branch subset metrics more than expected: vy={:?} vyyaw={:?}",
6085            vy_metrics,
6086            vyyaw_metrics
6087        );
6088        assert!(
6089            (vy_metrics.expanded_same_control0 - vyyaw_metrics.expanded_same_control0).abs()
6090                <= 1e-3
6091                && (vy_metrics.expanded_opposite_control0
6092                    - vyyaw_metrics.expanded_opposite_control0)
6093                    .abs()
6094                    <= 1e-3
6095                && (vy_metrics.expanded_opposite_pred_y - vyyaw_metrics.expanded_opposite_pred_y)
6096                    .abs()
6097                    <= 1e-4
6098                && (vy_metrics.expanded_opposite_pred_v - vyyaw_metrics.expanded_opposite_pred_v)
6099                    .abs()
6100                    <= 2e-4,
6101            "adding yaw to v+y changed the expanded-branch subset metrics more than expected: vy={:?} vyyaw={:?}",
6102            vy_metrics,
6103            vyyaw_metrics
6104        );
6105    }
6106
6107    #[test]
6108    #[ignore = "long-running regression scenario"]
6109    fn test_mpc_switch_back_full_candidate_expansion_characterizes_state_subset_per_step() {
6110        let steps = [325usize, 326, 327, 328, 329];
6111        let v_metrics = measure_switch_back_state_injection_by_step(&[2usize], &steps);
6112        let vy_metrics = measure_switch_back_state_injection_by_step(&[1usize, 2usize], &steps);
6113
6114        let step_325_v = state_injection_metrics_at_step(&v_metrics, 325);
6115        let step_325_vy = state_injection_metrics_at_step(&vy_metrics, 325);
6116        assert!(
6117            (step_325_v.default_opposite_control0 - step_325_vy.default_opposite_control0).abs()
6118                <= 5e-4,
6119            "default branch control moved too early at step 325: v={:?} vy={:?}",
6120            step_325_v,
6121            step_325_vy
6122        );
6123        assert!(
6124            step_325_v.default_opposite_pred_y > 10_000.0 * step_325_vy.default_opposite_pred_y,
6125            "default branch predicted-y should already need y at step 325: v={:?} vy={:?}",
6126            step_325_v,
6127            step_325_vy
6128        );
6129        assert!(
6130            step_325_v.expanded_opposite_pred_y > 1_000.0 * step_325_vy.expanded_opposite_pred_y,
6131            "expanded branch predicted-y should already improve with y at step 325: v={:?} vy={:?}",
6132            step_325_v,
6133            step_325_vy
6134        );
6135
6136        for step in [326usize, 327, 328, 329] {
6137            let v = state_injection_metrics_at_step(&v_metrics, step);
6138            let vy = state_injection_metrics_at_step(&vy_metrics, step);
6139
6140            assert!(
6141                v.default_opposite_control0 > 20.0 * vy.default_opposite_control0,
6142                "default branch control should need y from step {step}: v={:?} vy={:?}",
6143                v,
6144                vy
6145            );
6146            assert!(
6147                v.default_opposite_pred_y > 1_000.0 * vy.default_opposite_pred_y,
6148                "default branch predicted-y should strongly prefer v+y from step {step}: v={:?} vy={:?}",
6149                v,
6150                vy
6151            );
6152            assert!(
6153                v.expanded_opposite_pred_y > 1_000.0 * vy.expanded_opposite_pred_y,
6154                "expanded branch predicted-y should strongly prefer v+y from step {step}: v={:?} vy={:?}",
6155                v,
6156                vy
6157            );
6158        }
6159
6160        for step in [325usize, 326, 327, 328, 329] {
6161            let v = state_injection_metrics_at_step(&v_metrics, step);
6162            let vy = state_injection_metrics_at_step(&vy_metrics, step);
6163            assert!(
6164                (v.expanded_opposite_control0 - vy.expanded_opposite_control0).abs() <= 3e-3,
6165                "expanded branch control should stay mostly v-driven at step {step}: v={:?} vy={:?}",
6166                v,
6167                vy
6168            );
6169            assert!(
6170                (v.expanded_opposite_pred_v - vy.expanded_opposite_pred_v).abs() <= 6e-4,
6171                "expanded branch predicted-v should stay mostly v-driven at step {step}: v={:?} vy={:?}",
6172                v,
6173                vy
6174            );
6175        }
6176    }
6177
6178    /// Metrics for delta injection from step 325 → 326 transition analysis.
6179    #[derive(Debug)]
6180    struct StepTransitionDeltaMetrics {
6181        /// State delta from step 325 to step 326 (default branch).
6182        state_delta: Vector4<f64>,
6183        /// Baseline control[0] at step 325 (default branch).
6184        baseline_control0: [f64; 2],
6185        /// Expanded baseline control[0] at step 325.
6186        expanded_baseline_control0: [f64; 2],
6187        /// Per-component-set injection results: (component_label, injected_control0, same_diff, opposite_diff).
6188        injections: Vec<DeltaInjectionResult>,
6189    }
6190
6191    #[derive(Debug)]
6192    struct DeltaInjectionResult {
6193        label: &'static str,
6194        indices: Vec<usize>,
6195        injected_control0: [f64; 2],
6196        default_same_control0: f64,
6197        default_opposite_control0: f64,
6198        default_same_pred_y: f64,
6199        default_opposite_pred_y: f64,
6200        default_same_pred_v: f64,
6201        default_opposite_pred_v: f64,
6202    }
6203
6204    fn measure_step_transition_delta_injection() -> StepTransitionDeltaMetrics {
6205        let default_trace = collect_switch_back_detailed_trace_with_test_optimizer(
6206            330,
6207            optimize_linearized_controls_original,
6208        );
6209        let expanded_trace = collect_switch_back_detailed_trace_with_test_optimizer(
6210            330,
6211            optimize_linearized_controls_with_full_candidate_expansion,
6212        );
6213        let default_warm_start = collect_switch_back_warm_start_trace_with_test_optimizer(
6214            330,
6215            optimize_linearized_controls_original,
6216        );
6217
6218        let row_325 = detailed_row_at_step(&default_trace, 325);
6219        let row_326 = detailed_row_at_step(&default_trace, 326);
6220        let expanded_row_325 = detailed_row_at_step(&expanded_trace, 325);
6221
6222        let state_delta = row_326.state - row_325.state;
6223
6224        let default_baseline_controls: Vec<[f64; 2]> = row_325
6225            .controls
6226            .iter()
6227            .map(|vec| [vec[0], vec[1]])
6228            .collect();
6229        let expanded_baseline_controls: Vec<[f64; 2]> = expanded_row_325
6230            .controls
6231            .iter()
6232            .map(|vec| [vec[0], vec[1]])
6233            .collect();
6234        let default_baseline_predicted: Vec<[f64; 4]> = row_325
6235            .predicted
6236            .iter()
6237            .map(|vec| [vec[0], vec[1], vec[2], vec[3]])
6238            .collect();
6239        let expanded_baseline_predicted: Vec<[f64; 4]> = expanded_row_325
6240            .predicted
6241            .iter()
6242            .map(|vec| [vec[0], vec[1], vec[2], vec[3]])
6243            .collect();
6244
6245        let component_sets: Vec<(&str, Vec<usize>)> = vec![
6246            ("delta_v", vec![2]),
6247            ("delta_y", vec![1]),
6248            ("delta_v+delta_y", vec![1, 2]),
6249            ("delta_v+delta_y+delta_yaw", vec![1, 2, 3]),
6250        ];
6251
6252        let warm_start_325 = default_warm_start.get(&325).unwrap();
6253
6254        let mut injections = Vec::new();
6255        for (label, indices) in &component_sets {
6256            let mut modified_state_vec = row_325.state;
6257            for &idx in indices {
6258                modified_state_vec[idx] += state_delta[idx];
6259            }
6260            let modified_state = state_from_vector4(&modified_state_vec);
6261
6262            let result = iterative_linear_mpc_control_with_test_optimizer(
6263                &row_325.xref,
6264                &modified_state,
6265                warm_start_325,
6266                optimize_linearized_controls_original,
6267            );
6268
6269            let to_default_control =
6270                summarize_vec2_sequence_diff(&result.controls, &default_baseline_controls);
6271            let to_expanded_control =
6272                summarize_vec2_sequence_diff(&result.controls, &expanded_baseline_controls);
6273            let to_default_predicted =
6274                summarize_vec4_sequence_diff(&result.predicted, &default_baseline_predicted);
6275            let to_expanded_predicted =
6276                summarize_vec4_sequence_diff(&result.predicted, &expanded_baseline_predicted);
6277
6278            injections.push(DeltaInjectionResult {
6279                label,
6280                indices: indices.clone(),
6281                injected_control0: [result.controls[0][0], result.controls[0][1]],
6282                default_same_control0: to_default_control[0],
6283                default_opposite_control0: to_expanded_control[0],
6284                default_same_pred_y: to_default_predicted[1],
6285                default_opposite_pred_y: to_expanded_predicted[1],
6286                default_same_pred_v: to_default_predicted[2],
6287                default_opposite_pred_v: to_expanded_predicted[2],
6288            });
6289        }
6290
6291        StepTransitionDeltaMetrics {
6292            state_delta,
6293            baseline_control0: [row_325.controls[0][0], row_325.controls[0][1]],
6294            expanded_baseline_control0: [
6295                expanded_row_325.controls[0][0],
6296                expanded_row_325.controls[0][1],
6297            ],
6298            injections,
6299        }
6300    }
6301
6302    #[test]
6303    #[ignore = "long-running regression scenario"]
6304    fn test_mpc_switch_back_full_candidate_expansion_characterizes_step_325_326_transition() {
6305        let metrics = measure_step_transition_delta_injection();
6306
6307        println!("=== Step 325 → 326 Transition Delta Injection ===");
6308        println!("state_delta: {:?}", metrics.state_delta);
6309        println!(
6310            "baseline_control0 (default): {:?}",
6311            metrics.baseline_control0
6312        );
6313        println!(
6314            "baseline_control0 (expanded): {:?}",
6315            metrics.expanded_baseline_control0
6316        );
6317        for inj in &metrics.injections {
6318            println!("--- {} (indices {:?}) ---", inj.label, inj.indices);
6319            println!("  injected_control0: {:?}", inj.injected_control0);
6320            println!(
6321                "  same_control0: {:.6e}  opposite_control0: {:.6e}  ratio: {:.2}",
6322                inj.default_same_control0,
6323                inj.default_opposite_control0,
6324                if inj.default_opposite_control0 > 0.0 {
6325                    inj.default_same_control0 / inj.default_opposite_control0
6326                } else {
6327                    f64::INFINITY
6328                }
6329            );
6330            println!(
6331                "  same_pred_y: {:.6e}  opposite_pred_y: {:.6e}",
6332                inj.default_same_pred_y, inj.default_opposite_pred_y
6333            );
6334            println!(
6335                "  same_pred_v: {:.6e}  opposite_pred_v: {:.6e}",
6336                inj.default_same_pred_v, inj.default_opposite_pred_v
6337            );
6338        }
6339
6340        // delta_v alone: at step 325 v-only was sufficient, so applying
6341        // one step of delta_v should still keep control close to default baseline.
6342        let delta_v = &metrics.injections[0];
6343        assert!(
6344            delta_v.default_same_control0 < delta_v.default_opposite_control0,
6345            "delta_v at step 325 context should stay closer to default than expanded: {:?}",
6346            delta_v
6347        );
6348
6349        // delta_y alone: y alone had minimal control impact at step 325.
6350        let delta_y = &metrics.injections[1];
6351        assert!(
6352            delta_y.default_same_control0 < delta_y.default_opposite_control0,
6353            "delta_y at step 325 context should stay closer to default than expanded: {:?}",
6354            delta_y
6355        );
6356
6357        // delta_v+delta_y: the key question — does this combo start pulling
6358        // the control toward expanded, or does it stay default-like?
6359        // The handoff hypothesis: "delta_v + delta_y で step 326 の control が
6360        // 急に opposite 側へ寄る"
6361        let delta_vy = &metrics.injections[2];
6362        println!(
6363            "\n=== KEY: delta_v+delta_y same/opposite ratio = {:.4} ===",
6364            if delta_vy.default_opposite_control0 > 0.0 {
6365                delta_vy.default_same_control0 / delta_vy.default_opposite_control0
6366            } else {
6367                f64::INFINITY
6368            }
6369        );
6370
6371        // delta_v+delta_y+delta_yaw: should be nearly identical to delta_v+delta_y.
6372        let delta_vyy = &metrics.injections[3];
6373        let control0_diff =
6374            (delta_vy.default_same_control0 - delta_vyy.default_same_control0).abs();
6375        println!(
6376            "delta_v+delta_y vs delta_v+delta_y+delta_yaw control0 diff: {:.6e}",
6377            control0_diff
6378        );
6379    }
6380
6381    /// Result of v-only opposite-branch injection with context element swaps.
6382    #[derive(Debug)]
6383    struct ContextSwapInjectionResult {
6384        label: &'static str,
6385        injected_steer: f64,
6386        default_baseline_steer: f64,
6387        expanded_baseline_steer: f64,
6388        default_same_control0: f64,
6389        default_opposite_control0: f64,
6390        default_same_pred_y: f64,
6391        default_opposite_pred_y: f64,
6392        default_same_pred_v: f64,
6393        default_opposite_pred_v: f64,
6394    }
6395
6396    /// Investigate why v-only opposite-branch injection works at step 325 but
6397    /// fails at step 326 by systematically swapping context elements (state
6398    /// residual, xref, warm_start) between the two steps.
6399    fn measure_step_325_326_context_swap_v_injection() -> Vec<ContextSwapInjectionResult> {
6400        let default_trace = collect_switch_back_detailed_trace_with_test_optimizer(
6401            330,
6402            optimize_linearized_controls_original,
6403        );
6404        let expanded_trace = collect_switch_back_detailed_trace_with_test_optimizer(
6405            330,
6406            optimize_linearized_controls_with_full_candidate_expansion,
6407        );
6408        let default_warm_start = collect_switch_back_warm_start_trace_with_test_optimizer(
6409            330,
6410            optimize_linearized_controls_original,
6411        );
6412
6413        let d325 = detailed_row_at_step(&default_trace, 325);
6414        let d326 = detailed_row_at_step(&default_trace, 326);
6415        let e325 = detailed_row_at_step(&expanded_trace, 325);
6416        let e326 = detailed_row_at_step(&expanded_trace, 326);
6417        let ws325 = default_warm_start.get(&325).unwrap();
6418        let ws326 = default_warm_start.get(&326).unwrap();
6419
6420        let v_indices: &[usize] = &[2]; // v component only
6421
6422        // Helper to run v-only injection and compute metrics
6423        let run_injection = |label: &'static str,
6424                             default_state: &Vector4<f64>,
6425                             expanded_state: &Vector4<f64>,
6426                             xref: &[Vector4<f64>],
6427                             warm_start: &[Vector2<f64>],
6428                             default_row: &SwitchBackDetailedRow,
6429                             expanded_row: &SwitchBackDetailedRow| {
6430            let injected_state = inject_state_components(default_state, expanded_state, v_indices);
6431            let result = iterative_linear_mpc_control_with_test_optimizer(
6432                xref,
6433                &injected_state,
6434                warm_start,
6435                optimize_linearized_controls_original,
6436            );
6437
6438            let default_baseline_controls: Vec<[f64; 2]> = default_row
6439                .controls
6440                .iter()
6441                .map(|vec| [vec[0], vec[1]])
6442                .collect();
6443            let expanded_baseline_controls: Vec<[f64; 2]> = expanded_row
6444                .controls
6445                .iter()
6446                .map(|vec| [vec[0], vec[1]])
6447                .collect();
6448            let default_baseline_predicted: Vec<[f64; 4]> = default_row
6449                .predicted
6450                .iter()
6451                .map(|vec| [vec[0], vec[1], vec[2], vec[3]])
6452                .collect();
6453            let expanded_baseline_predicted: Vec<[f64; 4]> = expanded_row
6454                .predicted
6455                .iter()
6456                .map(|vec| [vec[0], vec[1], vec[2], vec[3]])
6457                .collect();
6458
6459            let to_default_control =
6460                summarize_vec2_sequence_diff(&result.controls, &default_baseline_controls);
6461            let to_expanded_control =
6462                summarize_vec2_sequence_diff(&result.controls, &expanded_baseline_controls);
6463            let to_default_predicted =
6464                summarize_vec4_sequence_diff(&result.predicted, &default_baseline_predicted);
6465            let to_expanded_predicted =
6466                summarize_vec4_sequence_diff(&result.predicted, &expanded_baseline_predicted);
6467
6468            ContextSwapInjectionResult {
6469                label,
6470                injected_steer: result.controls[0][1],
6471                default_baseline_steer: default_row.controls[0][1],
6472                expanded_baseline_steer: expanded_row.controls[0][1],
6473                default_same_control0: to_default_control[0],
6474                default_opposite_control0: to_expanded_control[0],
6475                default_same_pred_y: to_default_predicted[1],
6476                default_opposite_pred_y: to_expanded_predicted[1],
6477                default_same_pred_v: to_default_predicted[2],
6478                default_opposite_pred_v: to_expanded_predicted[2],
6479            }
6480        };
6481
6482        vec![
6483            // Baseline: v-only injection at step 325 (known to work)
6484            run_injection(
6485                "baseline_325: d325_state + e325_v, d325_xref, ws325",
6486                &d325.state,
6487                &e325.state,
6488                &d325.xref,
6489                ws325,
6490                d325,
6491                e325,
6492            ),
6493            // Baseline: v-only injection at step 326 (known to fail)
6494            run_injection(
6495                "baseline_326: d326_state + e326_v, d326_xref, ws326",
6496                &d326.state,
6497                &e326.state,
6498                &d326.xref,
6499                ws326,
6500                d326,
6501                e326,
6502            ),
6503            // Swap state residual: use step 325 default state (non-v) with step 326 expanded v
6504            run_injection(
6505                "swap_state: d325_state + e326_v, d326_xref, ws326",
6506                &d325.state,
6507                &e326.state,
6508                &d326.xref,
6509                ws326,
6510                d326,
6511                e326,
6512            ),
6513            // Swap xref: use step 326 state but step 325 xref
6514            run_injection(
6515                "swap_xref: d326_state + e326_v, d325_xref, ws326",
6516                &d326.state,
6517                &e326.state,
6518                &d325.xref,
6519                ws326,
6520                d326,
6521                e326,
6522            ),
6523            // Swap warm_start: use step 326 state/xref but step 325 warm_start
6524            run_injection(
6525                "swap_ws: d326_state + e326_v, d326_xref, ws325",
6526                &d326.state,
6527                &e326.state,
6528                &d326.xref,
6529                ws325,
6530                d326,
6531                e326,
6532            ),
6533            // Swap state + xref: step 325 state/xref with step 326 expanded v
6534            run_injection(
6535                "swap_state+xref: d325_state + e326_v, d325_xref, ws326",
6536                &d325.state,
6537                &e326.state,
6538                &d325.xref,
6539                ws326,
6540                d326,
6541                e326,
6542            ),
6543            // Swap state + warm_start
6544            run_injection(
6545                "swap_state+ws: d325_state + e326_v, d326_xref, ws325",
6546                &d325.state,
6547                &e326.state,
6548                &d326.xref,
6549                ws325,
6550                d326,
6551                e326,
6552            ),
6553            // Swap xref + warm_start
6554            run_injection(
6555                "swap_xref+ws: d326_state + e326_v, d325_xref, ws325",
6556                &d326.state,
6557                &e326.state,
6558                &d325.xref,
6559                ws325,
6560                d326,
6561                e326,
6562            ),
6563            // Swap all three: effectively step 325 context with step 326 expanded v
6564            run_injection(
6565                "swap_all: d325_state + e326_v, d325_xref, ws325",
6566                &d325.state,
6567                &e326.state,
6568                &d325.xref,
6569                ws325,
6570                d326,
6571                e326,
6572            ),
6573            // Cross-step expanded v: use step 325 expanded v in step 326 default context
6574            // to isolate whether expanded v progression matters
6575            run_injection(
6576                "cross_v_325in326: d326_state + e325_v, d326_xref, ws326",
6577                &d326.state,
6578                &e325.state,
6579                &d326.xref,
6580                ws326,
6581                d326,
6582                e326,
6583            ),
6584            // Cross-step expanded v: use step 326 expanded v in step 325 default context
6585            run_injection(
6586                "cross_v_326in325: d325_state + e326_v, d325_xref, ws325",
6587                &d325.state,
6588                &e326.state,
6589                &d325.xref,
6590                ws325,
6591                d325,
6592                e325,
6593            ),
6594            // --- Per-component isolation: starting from baseline_325 (d325 + e325_v),
6595            //     replace individual default non-v components with d326 values ---
6596            // Replace only x
6597            {
6598                let mut s = d325.state;
6599                s[0] = d326.state[0]; // x
6600                run_injection(
6601                    "isolate_x: d325{x→d326} + e325_v",
6602                    &s,
6603                    &e325.state,
6604                    &d325.xref,
6605                    ws325,
6606                    d325,
6607                    e325,
6608                )
6609            },
6610            // Replace only y
6611            {
6612                let mut s = d325.state;
6613                s[1] = d326.state[1]; // y
6614                run_injection(
6615                    "isolate_y: d325{y→d326} + e325_v",
6616                    &s,
6617                    &e325.state,
6618                    &d325.xref,
6619                    ws325,
6620                    d325,
6621                    e325,
6622                )
6623            },
6624            // Replace only yaw
6625            {
6626                let mut s = d325.state;
6627                s[3] = d326.state[3]; // yaw
6628                run_injection(
6629                    "isolate_yaw: d325{yaw→d326} + e325_v",
6630                    &s,
6631                    &e325.state,
6632                    &d325.xref,
6633                    ws325,
6634                    d325,
6635                    e325,
6636                )
6637            },
6638            // Replace y + yaw
6639            {
6640                let mut s = d325.state;
6641                s[1] = d326.state[1];
6642                s[3] = d326.state[3];
6643                run_injection(
6644                    "isolate_y+yaw: d325{y,yaw→d326} + e325_v",
6645                    &s,
6646                    &e325.state,
6647                    &d325.xref,
6648                    ws325,
6649                    d325,
6650                    e325,
6651                )
6652            },
6653            // Replace x + y
6654            {
6655                let mut s = d325.state;
6656                s[0] = d326.state[0];
6657                s[1] = d326.state[1];
6658                run_injection(
6659                    "isolate_x+y: d325{x,y→d326} + e325_v",
6660                    &s,
6661                    &e325.state,
6662                    &d325.xref,
6663                    ws325,
6664                    d325,
6665                    e325,
6666                )
6667            },
6668            // Replace all non-v (x + y + yaw) → should match cross_v_326in325
6669            {
6670                let mut s = d326.state;
6671                s[2] = d325.state[2]; // keep default v from step 325
6672                run_injection(
6673                    "isolate_all_nonv: d326{v→d325} + e325_v",
6674                    &s,
6675                    &e325.state,
6676                    &d325.xref,
6677                    ws325,
6678                    d325,
6679                    e325,
6680                )
6681            },
6682        ]
6683    }
6684
6685    #[test]
6686    #[ignore = "long-running regression scenario"]
6687    fn test_mpc_switch_back_characterizes_step_325_326_v_injection_context_swap() {
6688        let results = measure_step_325_326_context_swap_v_injection();
6689
6690        println!("=== Step 325 vs 326: V-Only Injection Context Swap ===");
6691        println!("Question: why does v-only opposite-branch injection work at 325 but not 326?");
6692        println!();
6693
6694        // Print raw state values for reference
6695        println!("=== Raw State Values ===");
6696        println!("  (indices: x=0, y=1, v=2, yaw=3)");
6697        // We need to recompute traces to get raw values for printing
6698        let default_trace_print = collect_switch_back_detailed_trace_with_test_optimizer(
6699            330,
6700            optimize_linearized_controls_original,
6701        );
6702        let expanded_trace_print = collect_switch_back_detailed_trace_with_test_optimizer(
6703            330,
6704            optimize_linearized_controls_with_full_candidate_expansion,
6705        );
6706        let d325_p = detailed_row_at_step(&default_trace_print, 325);
6707        let d326_p = detailed_row_at_step(&default_trace_print, 326);
6708        let e325_p = detailed_row_at_step(&expanded_trace_print, 325);
6709        let e326_p = detailed_row_at_step(&expanded_trace_print, 326);
6710        println!("  d325.state: {:?}", d325_p.state);
6711        println!("  d326.state: {:?}", d326_p.state);
6712        println!("  d325→d326 delta: {:?}", d326_p.state - d325_p.state);
6713        println!("  e325.state: {:?}", e325_p.state);
6714        println!("  e326.state: {:?}", e326_p.state);
6715        println!("  e325→e326 delta: {:?}", e326_p.state - e325_p.state);
6716        println!(
6717            "  v diff: e325.v={:.6} e326.v={:.6} delta={:.6}",
6718            e325_p.state[2],
6719            e326_p.state[2],
6720            e326_p.state[2] - e325_p.state[2]
6721        );
6722        println!();
6723
6724        for r in &results {
6725            let pull_ratio = if r.default_opposite_control0 > 0.0 {
6726                r.default_same_control0 / r.default_opposite_control0
6727            } else {
6728                f64::INFINITY
6729            };
6730            let pulls_toward_expanded = pull_ratio > 4.0;
6731            println!(
6732                "[{}] {} steer={:.6} (default={:.6}, expanded={:.6})",
6733                if pulls_toward_expanded {
6734                    "PULL"
6735                } else {
6736                    "STAY"
6737                },
6738                r.label,
6739                r.injected_steer,
6740                r.default_baseline_steer,
6741                r.expanded_baseline_steer,
6742            );
6743            println!(
6744                "  same_ctrl0={:.4e} opp_ctrl0={:.4e} ratio={:.3} pred_y_s={:.4e} pred_y_o={:.4e} pred_v_s={:.4e} pred_v_o={:.4e}",
6745                r.default_same_control0,
6746                r.default_opposite_control0,
6747                pull_ratio,
6748                r.default_same_pred_y,
6749                r.default_opposite_pred_y,
6750                r.default_same_pred_v,
6751                r.default_opposite_pred_v,
6752            );
6753        }
6754
6755        // Baseline assertions
6756        let baseline_325 = &results[0];
6757        let ratio_325 = baseline_325.default_same_control0 / baseline_325.default_opposite_control0;
6758        assert!(
6759            ratio_325 > 100.0,
6760            "v-only injection should strongly pull toward expanded at step 325 (ratio={ratio_325}): {:?}",
6761            baseline_325
6762        );
6763
6764        let baseline_326 = &results[1];
6765        let ratio_326 = baseline_326.default_same_control0 / baseline_326.default_opposite_control0;
6766        assert!(
6767            ratio_326 < 10.0,
6768            "v-only injection pull should weaken dramatically at step 326 (ratio={ratio_326}): {:?}",
6769            baseline_326
6770        );
6771
6772        // The ratio drop from 325 to 326 should be at least 100x
6773        assert!(
6774            ratio_325 > 100.0 * ratio_326,
6775            "v-only pull should drop >100x from step 325 to 326: 325={ratio_325} 326={ratio_326}"
6776        );
6777
6778        // xref and warm_start swaps should not change the ratio
6779        let swap_xref = &results[3];
6780        let ratio_swap_xref = swap_xref.default_same_control0 / swap_xref.default_opposite_control0;
6781        assert!(
6782            (ratio_swap_xref - ratio_326).abs() < 0.1,
6783            "xref swap should not change ratio: baseline={ratio_326} swapped={ratio_swap_xref}"
6784        );
6785
6786        let swap_ws = &results[4];
6787        let ratio_swap_ws = swap_ws.default_same_control0 / swap_ws.default_opposite_control0;
6788        assert!(
6789            (ratio_swap_ws - ratio_326).abs() < 0.1,
6790            "warm_start swap should not change ratio: baseline={ratio_326} swapped={ratio_swap_ws}"
6791        );
6792
6793        // state swap should change the ratio (state is the driver)
6794        let swap_state = &results[2];
6795        let ratio_swap_state =
6796            swap_state.default_same_control0 / swap_state.default_opposite_control0;
6797        assert!(
6798            (ratio_swap_state - ratio_326).abs() > 0.5,
6799            "state swap should change ratio: baseline={ratio_326} swapped={ratio_swap_state}"
6800        );
6801
6802        // Per-component isolation: none of the default non-v changes
6803        // should destroy the pull (all ratios should stay > 100)
6804        for result in &results[11..=16] {
6805            let ratio = result.default_same_control0 / result.default_opposite_control0;
6806            assert!(
6807                ratio > 100.0,
6808                "default non-v component change should not destroy pull: {} ratio={ratio}",
6809                result.label
6810            );
6811        }
6812    }
6813
6814    /// Run MPC with a fixed number of outer iterations to isolate where y's
6815    /// effect enters the control. If y only matters through nonlinear
6816    /// xbar re-linearization (outer loop), then 1-iteration solve should
6817    /// show no y effect, while 3-iteration solve should.
6818    fn iterative_linear_mpc_control_with_max_iter(
6819        xref: &[Vector4<f64>],
6820        state: &State,
6821        warm_start: &[Vector2<f64>],
6822        max_iter: usize,
6823    ) -> MpcResult {
6824        let mut controls = warm_start.to_vec();
6825        controls.resize(T, Vector2::zeros());
6826        apply_control_constraints(&mut controls);
6827
6828        let mut predicted = predict_motion(*state, &controls);
6829        for _ in 0..max_iter {
6830            let previous_controls = controls.clone();
6831            controls = optimize_linearized_controls(xref, &predicted, state, &controls);
6832            predicted = predict_motion(*state, &controls);
6833
6834            let du = controls
6835                .iter()
6836                .zip(previous_controls.iter())
6837                .map(|(current, previous)| {
6838                    (current[0] - previous[0]).abs() + (current[1] - previous[1]).abs()
6839                })
6840                .sum::<f64>();
6841
6842            if du <= DU_TH {
6843                break;
6844            }
6845        }
6846
6847        MpcResult {
6848            controls,
6849            predicted,
6850        }
6851    }
6852
6853    /// Dump the first inner-solve adjoint to trace how y reaches steering gradient.
6854    /// Returns (steering_gradient, lambda3_components) for analysis.
6855    fn dump_first_inner_solve_adjoint(
6856        xref: &[Vector4<f64>],
6857        state: &State,
6858        warm_start: &[Vector2<f64>],
6859    ) -> (Vec<Vector2<f64>>, Vec<[f64; 5]>) {
6860        let mut controls = warm_start.to_vec();
6861        controls.resize(T, Vector2::zeros());
6862        apply_control_constraints(&mut controls);
6863
6864        let xbar = predict_motion(*state, &controls);
6865
6866        let q = state_cost_weight();
6867        let qf = terminal_cost_weight();
6868        let r = control_cost_weight();
6869        let rd = control_rate_weight();
6870
6871        let (x, a_seq, b_seq, _) = linearized_rollout(state, &xbar, &controls);
6872
6873        let mut gradients = vec![Vector2::zeros(); T];
6874        let mut lambda = (qf * state_error(x[T], xref[T])) * 2.0;
6875        let mut lambda3_decomp = Vec::new();
6876
6877        // Terminal lambda[3] decomposition
6878        lambda3_decomp.push([
6879            lambda[3], // total lambda[3]
6880            0.0,       // from lx[3]
6881            0.0,       // from A^T[3,0]*lambda_next[0]
6882            0.0,       // from A^T[3,1]*lambda_next[1]
6883            lambda[3], // from propagation (terminal)
6884        ]);
6885
6886        for t in (0..T).rev() {
6887            let mut grad = (r * controls[t]) * 2.0 + b_seq[t].transpose() * lambda;
6888            if t > 0 {
6889                grad += (rd * (controls[t] - controls[t - 1])) * 2.0;
6890            }
6891            if t < T - 1 {
6892                grad -= (rd * (controls[t + 1] - controls[t])) * 2.0;
6893            }
6894            gradients[t] = grad;
6895
6896            let lx = if t == 0 {
6897                Vector4::zeros()
6898            } else {
6899                (q * state_error(x[t], xref[t])) * 2.0
6900            };
6901
6902            let lambda_next = lambda;
6903            lambda = lx + a_seq[t].transpose() * lambda_next;
6904
6905            // Decompose lambda[3]
6906            let from_lx3 = lx[3];
6907            let from_at30_l0 = a_seq[t][(0, 3)] * lambda_next[0]; // A^T[3,0] = A[0,3]
6908            let from_at31_l1 = a_seq[t][(1, 3)] * lambda_next[1]; // A^T[3,1] = A[1,3]
6909            let from_at33_l3 = a_seq[t][(3, 3)] * lambda_next[3]; // A^T[3,3] = A[3,3]
6910            lambda3_decomp.push([
6911                lambda[3],
6912                from_lx3,
6913                from_at30_l0,
6914                from_at31_l1,
6915                from_at33_l3,
6916            ]);
6917        }
6918
6919        lambda3_decomp.reverse();
6920        (gradients, lambda3_decomp)
6921    }
6922
6923    #[test]
6924    #[ignore = "long-running regression scenario"]
6925    fn test_mpc_switch_back_characterizes_y_effect_path_via_outer_loop() {
6926        let default_trace = collect_switch_back_detailed_trace_with_test_optimizer(
6927            330,
6928            optimize_linearized_controls_original,
6929        );
6930        let expanded_trace = collect_switch_back_detailed_trace_with_test_optimizer(
6931            330,
6932            optimize_linearized_controls_with_full_candidate_expansion,
6933        );
6934        let default_warm_start = collect_switch_back_warm_start_trace_with_test_optimizer(
6935            330,
6936            optimize_linearized_controls_original,
6937        );
6938
6939        println!("=== Y Effect Path: Outer Loop Iteration Count ===");
6940        println!("At step 326, v-only injection needs v+y. Is y's effect through outer-loop re-linearization?");
6941        println!();
6942
6943        for &step in &[325usize, 326, 327] {
6944            let d_row = detailed_row_at_step(&default_trace, step);
6945            let e_row = detailed_row_at_step(&expanded_trace, step);
6946            let ws = default_warm_start.get(&step).unwrap();
6947
6948            let d_baseline_steer = d_row.controls[0][1];
6949            let e_baseline_steer = e_row.controls[0][1];
6950
6951            println!("--- step {step} (default_steer={d_baseline_steer:.6}, expanded_steer={e_baseline_steer:.6}) ---");
6952
6953            for &max_iter in &[1usize, 2, 3] {
6954                // v-only injection
6955                let v_state = inject_state_components(&d_row.state, &e_row.state, &[2]);
6956                let v_result =
6957                    iterative_linear_mpc_control_with_max_iter(&d_row.xref, &v_state, ws, max_iter);
6958
6959                // v+y injection
6960                let vy_state = inject_state_components(&d_row.state, &e_row.state, &[1, 2]);
6961                let vy_result = iterative_linear_mpc_control_with_max_iter(
6962                    &d_row.xref,
6963                    &vy_state,
6964                    ws,
6965                    max_iter,
6966                );
6967
6968                let v_steer = v_result.controls[0][1];
6969                let vy_steer = vy_result.controls[0][1];
6970                let steer_diff = (v_steer - vy_steer).abs();
6971
6972                println!(
6973                    "  iter={max_iter}: v_steer={v_steer:.6} vy_steer={vy_steer:.6} diff={steer_diff:.6e}"
6974                );
6975            }
6976        }
6977    }
6978
6979    /// linearized_rollout WITHOUT angle normalization — matches Python CVXPY behavior.
6980    fn linearized_rollout_without_angle_normalization(
6981        x0: &State,
6982        xbar: &[Vector4<f64>],
6983        controls: &[Vector2<f64>],
6984    ) -> LinearizedRollout {
6985        let mut x = vec![Vector4::zeros(); T + 1];
6986        let mut a_seq = Vec::with_capacity(T);
6987        let mut b_seq = Vec::with_capacity(T);
6988        let mut c_seq = Vec::with_capacity(T);
6989
6990        x[0] = x0.to_vector();
6991        for t in 0..T {
6992            let (a, b, c) = get_linear_model_matrix(xbar[t][2], xbar[t][3], 0.0);
6993            let next = a * x[t] + b * controls[t] + c;
6994            // No normalize_angle here — Python CVXPY doesn't do this
6995
6996            a_seq.push(a);
6997            b_seq.push(b);
6998            c_seq.push(c);
6999            x[t + 1] = next;
7000        }
7001
7002        (x, a_seq, b_seq, c_seq)
7003    }
7004
7005    #[test]
7006    #[ignore = "long-running regression scenario"]
7007    fn test_mpc_switch_back_characterizes_yaw_normalize_effect_in_linearized_rollout() {
7008        let default_trace = collect_switch_back_detailed_trace_with_test_optimizer(
7009            330,
7010            optimize_linearized_controls,
7011        );
7012        let default_warm_start = collect_switch_back_warm_start_trace_with_test_optimizer(
7013            330,
7014            optimize_linearized_controls,
7015        );
7016
7017        println!("=== Yaw Normalize Effect in Linearized Rollout ===");
7018        println!("Comparing linearized_rollout with and without normalize_angle");
7019        println!();
7020
7021        for &step in &[20usize, 70, 71, 75, 78, 325, 326] {
7022            let row = detailed_row_at_step(&default_trace, step);
7023            let ws = default_warm_start.get(&step).unwrap();
7024            let state = state_from_vector4(&row.state);
7025
7026            // Build xbar from predict_motion (nonlinear, as both Rust and Python do)
7027            let mut controls = ws.to_vec();
7028            controls.resize(T, Vector2::zeros());
7029            apply_control_constraints(&mut controls);
7030            let xbar = predict_motion(state, &controls);
7031
7032            let (x_with, _, _, _) = linearized_rollout(&state, &xbar, &controls);
7033            let (x_without, _, _, _) =
7034                linearized_rollout_without_angle_normalization(&state, &xbar, &controls);
7035
7036            let mut max_yaw_diff = 0.0f64;
7037            let mut max_other_diff = [0.0f64; 3]; // x, y, v
7038            for t in 0..=T {
7039                let yaw_diff = (x_with[t][3] - x_without[t][3]).abs();
7040                max_yaw_diff = max_yaw_diff.max(yaw_diff);
7041                for (i, slot) in max_other_diff.iter_mut().enumerate() {
7042                    *slot = (*slot).max((x_with[t][i] - x_without[t][i]).abs());
7043                }
7044            }
7045
7046            let yaw_values: Vec<f64> = (0..=T).map(|t| x_with[t][3]).collect();
7047            println!(
7048                "step {step:>3}: yaw_diff={max_yaw_diff:.4e} other=[{:.4e},{:.4e},{:.4e}] yaw={:.4?}",
7049                max_other_diff[0], max_other_diff[1], max_other_diff[2], yaw_values
7050            );
7051        }
7052    }
7053
7054    #[test]
7055    #[ignore = "long-running regression scenario"]
7056    fn test_mpc_switch_back_characterizes_adjoint_y_to_steer_coupling() {
7057        let default_trace = collect_switch_back_detailed_trace_with_test_optimizer(
7058            330,
7059            optimize_linearized_controls_original,
7060        );
7061        let expanded_trace = collect_switch_back_detailed_trace_with_test_optimizer(
7062            330,
7063            optimize_linearized_controls_with_full_candidate_expansion,
7064        );
7065        let default_warm_start = collect_switch_back_warm_start_trace_with_test_optimizer(
7066            330,
7067            optimize_linearized_controls_original,
7068        );
7069
7070        println!("=== Adjoint Decomposition: How Y Reaches Steering ===");
7071        println!(
7072            "lambda3_decomp columns: [total, from_lx3, from_AT30*l0, from_AT31*l1, from_AT33*l3]"
7073        );
7074        println!();
7075
7076        for &step in &[325usize, 326] {
7077            let d_row = detailed_row_at_step(&default_trace, step);
7078            let e_row = detailed_row_at_step(&expanded_trace, step);
7079            let ws = default_warm_start.get(&step).unwrap();
7080
7081            println!("--- step {step} ---");
7082
7083            // Baseline (no injection)
7084            let (grad_base, l3_base) =
7085                dump_first_inner_solve_adjoint(&d_row.xref, &state_from_vector4(&d_row.state), ws);
7086            println!("  [baseline] steer_grad[0]={:.6e}", grad_base[0][1]);
7087            for (t, decomp) in l3_base.iter().enumerate() {
7088                println!(
7089                    "    t={t}: λ3={:.4e} lx3={:.4e} AT30·l0={:.4e} AT31·l1={:.4e} AT33·l3={:.4e}",
7090                    decomp[0], decomp[1], decomp[2], decomp[3], decomp[4]
7091                );
7092            }
7093
7094            // v-only injection
7095            let v_state = inject_state_components(&d_row.state, &e_row.state, &[2]);
7096            let (grad_v, l3_v) = dump_first_inner_solve_adjoint(&d_row.xref, &v_state, ws);
7097            println!("  [v-only] steer_grad[0]={:.6e}", grad_v[0][1]);
7098            for (t, decomp) in l3_v.iter().enumerate() {
7099                println!(
7100                    "    t={t}: λ3={:.4e} lx3={:.4e} AT30·l0={:.4e} AT31·l1={:.4e} AT33·l3={:.4e}",
7101                    decomp[0], decomp[1], decomp[2], decomp[3], decomp[4]
7102                );
7103            }
7104
7105            // v+y injection
7106            let vy_state = inject_state_components(&d_row.state, &e_row.state, &[1, 2]);
7107            let (grad_vy, l3_vy) = dump_first_inner_solve_adjoint(&d_row.xref, &vy_state, ws);
7108            println!("  [v+y] steer_grad[0]={:.6e}", grad_vy[0][1]);
7109            for (t, decomp) in l3_vy.iter().enumerate() {
7110                println!(
7111                    "    t={t}: λ3={:.4e} lx3={:.4e} AT30·l0={:.4e} AT31·l1={:.4e} AT33·l3={:.4e}",
7112                    decomp[0], decomp[1], decomp[2], decomp[3], decomp[4]
7113                );
7114            }
7115
7116            // Print gradient differences
7117            println!(
7118                "  steer_grad[0] diffs: v-base={:.6e} vy-base={:.6e} vy-v={:.6e}",
7119                grad_v[0][1] - grad_base[0][1],
7120                grad_vy[0][1] - grad_base[0][1],
7121                grad_vy[0][1] - grad_v[0][1],
7122            );
7123            println!();
7124        }
7125    }
7126
7127    /// Per-component state crossover: replace individual state components
7128    /// from step 325 into step 326 context to identify which component(s)
7129    /// of the state change drive the v-only pull degradation.
7130    #[test]
7131    #[ignore = "long-running regression scenario"]
7132    fn test_mpc_switch_back_characterizes_step_325_326_state_component_crossover() {
7133        let default_trace = collect_switch_back_detailed_trace_with_test_optimizer(
7134            330,
7135            optimize_linearized_controls_original,
7136        );
7137        let expanded_trace = collect_switch_back_detailed_trace_with_test_optimizer(
7138            330,
7139            optimize_linearized_controls_with_full_candidate_expansion,
7140        );
7141        let default_warm_start = collect_switch_back_warm_start_trace_with_test_optimizer(
7142            330,
7143            optimize_linearized_controls_original,
7144        );
7145
7146        let d325 = detailed_row_at_step(&default_trace, 325);
7147        let d326 = detailed_row_at_step(&default_trace, 326);
7148        let e326 = detailed_row_at_step(&expanded_trace, 326);
7149        let ws326 = default_warm_start.get(&326).unwrap();
7150
7151        let state_delta = d326.state - d325.state;
7152        println!(
7153            "state_delta: x={:.6e} y={:.6e} v={:.6e} yaw={:.6e}",
7154            state_delta[0], state_delta[1], state_delta[2], state_delta[3]
7155        );
7156
7157        let expanded_baseline_controls: Vec<[f64; 2]> =
7158            e326.controls.iter().map(|v| [v[0], v[1]]).collect();
7159        let default_baseline_controls: Vec<[f64; 2]> =
7160            d326.controls.iter().map(|v| [v[0], v[1]]).collect();
7161
7162        let component_sets: Vec<(&str, Vec<usize>)> = vec![
7163            ("revert_x", vec![0]),
7164            ("revert_y", vec![1]),
7165            ("revert_v", vec![2]),
7166            ("revert_yaw", vec![3]),
7167            ("revert_y+v", vec![1, 2]),
7168            ("revert_x+yaw", vec![0, 3]),
7169            ("revert_all", vec![0, 1, 2, 3]),
7170        ];
7171
7172        println!(
7173            "\n=== State Component Crossover (step 326 context, revert components to 325) ==="
7174        );
7175
7176        // Baseline: v-only injection at step 326
7177        let baseline_injected_state = inject_state_components(&d326.state, &e326.state, &[2]);
7178        let baseline_result = iterative_linear_mpc_control_with_test_optimizer(
7179            &d326.xref,
7180            &baseline_injected_state,
7181            ws326,
7182            optimize_linearized_controls_original,
7183        );
7184        let baseline_to_default =
7185            summarize_vec2_sequence_diff(&baseline_result.controls, &default_baseline_controls);
7186        let baseline_to_expanded =
7187            summarize_vec2_sequence_diff(&baseline_result.controls, &expanded_baseline_controls);
7188        let baseline_ratio = if baseline_to_expanded[0] > 0.0 {
7189            baseline_to_default[0] / baseline_to_expanded[0]
7190        } else {
7191            f64::INFINITY
7192        };
7193        println!(
7194            "  baseline: same={:.4e} opp={:.4e} ratio={:.3}",
7195            baseline_to_default[0], baseline_to_expanded[0], baseline_ratio
7196        );
7197
7198        let mut ratios = Vec::new();
7199        for (label, indices) in &component_sets {
7200            let mut modified_default = d326.state;
7201            for &idx in indices {
7202                modified_default[idx] = d325.state[idx];
7203            }
7204
7205            let injected_state = inject_state_components(&modified_default, &e326.state, &[2]);
7206            let result = iterative_linear_mpc_control_with_test_optimizer(
7207                &d326.xref,
7208                &injected_state,
7209                ws326,
7210                optimize_linearized_controls_original,
7211            );
7212
7213            let to_default =
7214                summarize_vec2_sequence_diff(&result.controls, &default_baseline_controls);
7215            let to_expanded =
7216                summarize_vec2_sequence_diff(&result.controls, &expanded_baseline_controls);
7217            let ratio = if to_expanded[0] > 0.0 {
7218                to_default[0] / to_expanded[0]
7219            } else {
7220                f64::INFINITY
7221            };
7222
7223            println!(
7224                "  {:12} same={:.4e} opp={:.4e} ratio={:.3} (delta: {:.3})",
7225                label,
7226                to_default[0],
7227                to_expanded[0],
7228                ratio,
7229                ratio - baseline_ratio
7230            );
7231            ratios.push((*label, ratio));
7232        }
7233
7234        let revert_y_ratio = ratios.iter().find(|(l, _)| *l == "revert_y").unwrap().1;
7235        let revert_v_ratio = ratios.iter().find(|(l, _)| *l == "revert_v").unwrap().1;
7236        let revert_yv_ratio = ratios.iter().find(|(l, _)| *l == "revert_y+v").unwrap().1;
7237        let revert_all_ratio = ratios.iter().find(|(l, _)| *l == "revert_all").unwrap().1;
7238
7239        println!("\n=== Summary ===");
7240        println!("baseline_326 ratio: {:.3}", baseline_ratio);
7241        println!("revert_y ratio:     {:.3}", revert_y_ratio);
7242        println!("revert_v ratio:     {:.3}", revert_v_ratio);
7243        println!("revert_y+v ratio:   {:.3}", revert_yv_ratio);
7244        println!("revert_all ratio:   {:.3}", revert_all_ratio);
7245
7246        // y alone accounts for nearly all the effect
7247        assert!(
7248            (revert_y_ratio - revert_all_ratio).abs() < 0.1,
7249            "reverting y alone should account for nearly all the effect: y={revert_y_ratio} all={revert_all_ratio}"
7250        );
7251
7252        // v contributes nothing
7253        assert!(
7254            (revert_v_ratio - baseline_ratio).abs() < 0.01,
7255            "reverting v should have no effect: v={revert_v_ratio} baseline={baseline_ratio}"
7256        );
7257
7258        // x and yaw contribute nothing
7259        let revert_xyaw_ratio = ratios.iter().find(|(l, _)| *l == "revert_x+yaw").unwrap().1;
7260        assert!(
7261            (revert_xyaw_ratio - baseline_ratio).abs() < 0.01,
7262            "reverting x+yaw should have no effect: x+yaw={revert_xyaw_ratio} baseline={baseline_ratio}"
7263        );
7264    }
7265
7266    /// Print state.y divergence history between default and expanded branches
7267    /// to identify when and how the lateral position diverges.
7268    #[test]
7269    #[ignore = "long-running regression scenario"]
7270    fn test_mpc_switch_back_characterizes_y_divergence_history() {
7271        let default_trace = collect_switch_back_detailed_trace_with_test_optimizer(
7272            350,
7273            optimize_linearized_controls_original,
7274        );
7275        let expanded_trace = collect_switch_back_detailed_trace_with_test_optimizer(
7276            350,
7277            optimize_linearized_controls_with_full_candidate_expansion,
7278        );
7279
7280        println!("=== state.y Divergence History (default vs expanded) ===");
7281        println!(
7282            "{:>5} {:>12} {:>12} {:>12} {:>12} {:>12}",
7283            "step", "default_y", "expanded_y", "y_diff", "default_v", "v_diff"
7284        );
7285
7286        let mut first_significant_step = None;
7287        let mut max_y_diff: f64 = 0.0;
7288
7289        for step in (0..350).step_by(5) {
7290            let d = default_trace.iter().find(|r| r.step == step);
7291            let e = expanded_trace.iter().find(|r| r.step == step);
7292            if let (Some(d), Some(e)) = (d, e) {
7293                let y_diff = (d.state[1] - e.state[1]).abs();
7294                max_y_diff = max_y_diff.max(y_diff);
7295
7296                if y_diff > 1e-6 || step >= 60 {
7297                    println!(
7298                        "{:5} {:12.6} {:12.6} {:12.6e} {:12.6} {:12.6e}",
7299                        step,
7300                        d.state[1],
7301                        e.state[1],
7302                        d.state[1] - e.state[1],
7303                        d.state[2],
7304                        d.state[2] - e.state[2]
7305                    );
7306                }
7307
7308                if first_significant_step.is_none() && y_diff > 1e-3 {
7309                    first_significant_step = Some(step);
7310                }
7311            }
7312        }
7313
7314        // Dense print around the first significant divergence
7315        if let Some(start) = first_significant_step {
7316            let dense_start = start.saturating_sub(10);
7317            println!(
7318                "\n=== Dense view around first significant y divergence (step {}) ===",
7319                start
7320            );
7321            for step in dense_start..=(start + 20).min(349) {
7322                let d = default_trace.iter().find(|r| r.step == step);
7323                let e = expanded_trace.iter().find(|r| r.step == step);
7324                if let (Some(d), Some(e)) = (d, e) {
7325                    let y_diff = d.state[1] - e.state[1];
7326                    let v_diff = d.state[2] - e.state[2];
7327                    let steer_diff = d.controls[0][1] - e.controls[0][1];
7328                    println!(
7329                        "  step {:3}: y_diff={:12.6e} v_diff={:12.6e} steer_diff={:12.6e}",
7330                        step, y_diff, v_diff, steer_diff
7331                    );
7332                }
7333            }
7334        }
7335
7336        // Dense print around step 320-330 (the pre-terminal region)
7337        println!("\n=== Dense view step 315..335 (pre-terminal) ===");
7338        for step in 315..=335 {
7339            let d = default_trace.iter().find(|r| r.step == step);
7340            let e = expanded_trace.iter().find(|r| r.step == step);
7341            if let (Some(d), Some(e)) = (d, e) {
7342                let y_diff = d.state[1] - e.state[1];
7343                let v_diff = d.state[2] - e.state[2];
7344                let steer_diff = d.controls[0][1] - e.controls[0][1];
7345                println!(
7346                    "  step {:3}: y={:10.6}/{:10.6} diff={:12.6e} v_diff={:12.6e} steer_diff={:12.6e}",
7347                    step, d.state[1], e.state[1], y_diff, v_diff, steer_diff
7348                );
7349            }
7350        }
7351
7352        println!("\nmax |y_diff| across all steps: {:.6e}", max_y_diff);
7353        if let Some(s) = first_significant_step {
7354            println!("first step with |y_diff| > 1e-3: {}", s);
7355        }
7356    }
7357
7358    /// Optimizer without angle wrap, matching `optimize_linearized_controls` signature
7359    /// for use with `iterative_linear_mpc_control_with_test_optimizer`.
7360    fn optimize_linearized_controls_without_angle_wrap(
7361        xref: &[Vector4<f64>],
7362        xbar: &[Vector4<f64>],
7363        state: &State,
7364        initial_controls: &[Vector2<f64>],
7365    ) -> Vec<Vector2<f64>> {
7366        let (controls, _) = optimize_linearized_controls_without_angle_wrap_with_trace(
7367            xref,
7368            xbar,
7369            state,
7370            initial_controls,
7371            QP_MAX_ITERS,
7372            LINE_SEARCH_ITERS,
7373        );
7374        controls
7375    }
7376
7377    #[test]
7378    #[ignore = "long-running regression scenario"]
7379    fn test_mpc_switch_back_no_angle_wrap_closed_loop_parity() {
7380        // Python reference values (from solver gap memo)
7381        let python_final_state = [
7382            -0.014_888_855_512_389,
7383            0.698_023_990_780_443,
7384            0.132_179_560_675_735,
7385            1.589_745_981_585_03,
7386        ];
7387        let python_history_length = 347usize;
7388
7389        // Run default (with angle wrap) closed-loop
7390        let default_result = run_mpc_simulation();
7391        let default_final = default_result.final_state.to_vector();
7392        let default_len = default_result.hist_x.len();
7393
7394        // Run no-angle-wrap closed-loop
7395        let (cx, cy, cyaw) = generate_switch_back_course();
7396        let speed_profile = calc_speed_profile(&cx, &cy, &cyaw, TARGET_SPEED);
7397
7398        let mut state = State::new(cx[0], cy[0], 0.0, cyaw[0]);
7399        if state.yaw - cyaw[0] >= PI {
7400            state.yaw -= 2.0 * PI;
7401        } else if state.yaw - cyaw[0] <= -PI {
7402            state.yaw += 2.0 * PI;
7403        }
7404
7405        let final_index = cx.len() - 1;
7406        let goal = (cx[final_index], cy[final_index]);
7407        let mut target_index = 0;
7408        let mut warm_start = vec![Vector2::zeros(); T];
7409        let mut step_count = 0;
7410
7411        for _ in 0..MAX_SIM_STEPS {
7412            let (xref, new_index) = calc_ref_trajectory(
7413                &state,
7414                &cx,
7415                &cy,
7416                &cyaw,
7417                &speed_profile,
7418                UPSTREAM_SWITCH_BACK_TICK,
7419                target_index,
7420            );
7421            target_index = new_index;
7422
7423            let result = iterative_linear_mpc_control_with_test_optimizer(
7424                &xref,
7425                &state,
7426                &warm_start,
7427                optimize_linearized_controls_without_angle_wrap,
7428            );
7429
7430            state.update(result.controls[0][0], result.controls[0][1]);
7431            warm_start = next_warm_start(&result.controls);
7432            step_count += 1;
7433
7434            if check_goal(&state, goal, target_index, final_index) {
7435                break;
7436            }
7437        }
7438
7439        let nowrap_final = state.to_vector();
7440        let nowrap_len = step_count + 1; // +1 for initial state
7441
7442        // Compute diffs against Python reference
7443        let default_diffs: Vec<f64> = (0..4)
7444            .map(|i| (default_final[i] - python_final_state[i]).abs())
7445            .collect();
7446        let nowrap_diffs: Vec<f64> = (0..4)
7447            .map(|i| (nowrap_final[i] - python_final_state[i]).abs())
7448            .collect();
7449
7450        println!("=== Closed-Loop Parity: Default vs No-Angle-Wrap ===");
7451        println!(
7452            "Python reference: {:?} (len={})",
7453            python_final_state, python_history_length
7454        );
7455        println!();
7456        println!("Default (with normalize_angle):");
7457        println!(
7458            "  final: [{:.6}, {:.6}, {:.6}, {:.6}] (len={})",
7459            default_final[0], default_final[1], default_final[2], default_final[3], default_len
7460        );
7461        println!(
7462            "  diffs: [{:.6e}, {:.6e}, {:.6e}, {:.6e}] step_diff={}",
7463            default_diffs[0],
7464            default_diffs[1],
7465            default_diffs[2],
7466            default_diffs[3],
7467            (default_len as isize - python_history_length as isize).abs()
7468        );
7469        println!();
7470        println!("No-angle-wrap:");
7471        println!(
7472            "  final: [{:.6}, {:.6}, {:.6}, {:.6}] (len={})",
7473            nowrap_final[0], nowrap_final[1], nowrap_final[2], nowrap_final[3], nowrap_len
7474        );
7475        println!(
7476            "  diffs: [{:.6e}, {:.6e}, {:.6e}, {:.6e}] step_diff={}",
7477            nowrap_diffs[0],
7478            nowrap_diffs[1],
7479            nowrap_diffs[2],
7480            nowrap_diffs[3],
7481            (nowrap_len as isize - python_history_length as isize).abs()
7482        );
7483        println!();
7484
7485        // Compare total state diff
7486        let default_total: f64 = default_diffs.iter().sum();
7487        let nowrap_total: f64 = nowrap_diffs.iter().sum();
7488        println!(
7489            "Total state diff: default={:.6e} nowrap={:.6e} improvement={:.2}%",
7490            default_total,
7491            nowrap_total,
7492            if default_total > 0.0 {
7493                (1.0 - nowrap_total / default_total) * 100.0
7494            } else {
7495                0.0
7496            }
7497        );
7498    }
7499
7500    /// Solve the MPC QP using clarabel, matching the Python CVXPY formulation exactly.
7501    ///
7502    /// Decision variables: z = [u_flat (2*T), x_flat (4*(T+1))]
7503    ///   u_flat = [a0, δ0, a1, δ1, ..., a_{T-1}, δ_{T-1}]
7504    ///   x_flat = [x0_0..x0_3, x1_0..x1_3, ..., xT_0..xT_3]
7505    ///
7506    /// Cost: Σ u[t]'Ru[t] + Σ_{t=1..T-1} (x[t]-xref[t])'Q(x[t]-xref[t])
7507    ///       + (x[T]-xref[T])'Qf(x[T]-xref[T]) + Σ (u[t+1]-u[t])'Rd(u[t+1]-u[t])
7508    ///
7509    /// Constraints:
7510    ///   x[t+1] = A[t]*x[t] + B[t]*u[t] + C[t]   (equality)
7511    ///   x[0] = x0                                  (equality)
7512    ///   -MAX_ACCEL <= u[0,t] <= MAX_ACCEL          (box)
7513    ///   -MAX_STEER <= u[1,t] <= MAX_STEER          (box)
7514    ///   |u[1,t+1] - u[1,t]| <= MAX_DSTEER*DT      (box)
7515    ///   MIN_SPEED <= x[2,t] <= MAX_SPEED            (box)
7516    #[allow(clippy::needless_range_loop, clippy::type_complexity)]
7517    fn solve_mpc_qp_clarabel(
7518        xref: &[Vector4<f64>],
7519        xbar: &[Vector4<f64>],
7520        state: &State,
7521    ) -> Option<(Vec<Vector2<f64>>, Vec<Vector4<f64>>)> {
7522        use clarabel::algebra::*;
7523        use clarabel::solver::*;
7524
7525        let nu = 2; // controls per step
7526        let nx = 4; // state dim
7527        let n_u = nu * T; // total control vars: 10
7528        let n_x = nx * (T + 1); // total state vars: 24
7529        let n = n_u + n_x; // total vars: 34
7530
7531        // Variable index helpers
7532        let u_idx = |t: usize, k: usize| -> usize { t * nu + k };
7533        let x_idx = |t: usize, k: usize| -> usize { n_u + t * nx + k };
7534
7535        // --- Build P (cost Hessian, upper triangular) ---
7536        let r_diag = R;
7537        let rd_diag = RD;
7538        let q_diag = Q;
7539        let qf_diag = QF;
7540
7541        let mut p_rows = Vec::new();
7542        let mut p_cols = Vec::new();
7543        let mut p_vals = Vec::new();
7544
7545        // Control cost: R
7546        for t in 0..T {
7547            for k in 0..nu {
7548                let idx = u_idx(t, k);
7549                let mut val = r_diag[k];
7550                // Rate cost: Rd contributes to diagonal
7551                if t > 0 {
7552                    val += rd_diag[k];
7553                }
7554                if t < T - 1 {
7555                    val += rd_diag[k];
7556                }
7557                p_rows.push(idx);
7558                p_cols.push(idx);
7559                p_vals.push(val * 2.0); // clarabel uses 0.5*z'Pz, so P = 2*H
7560            }
7561            // Rate cost off-diagonal: -Rd between consecutive controls
7562            if t < T - 1 {
7563                for k in 0..nu {
7564                    let i = u_idx(t, k);
7565                    let j = u_idx(t + 1, k);
7566                    // Upper triangular: i < j
7567                    p_rows.push(i);
7568                    p_cols.push(j);
7569                    p_vals.push(-rd_diag[k] * 2.0);
7570                }
7571            }
7572        }
7573
7574        // State cost: Q for t=1..T-1, Qf for t=T
7575        for t in 1..=T {
7576            let w = if t == T { qf_diag } else { q_diag };
7577            for k in 0..nx {
7578                let idx = x_idx(t, k);
7579                p_rows.push(idx);
7580                p_cols.push(idx);
7581                p_vals.push(w[k] * 2.0);
7582            }
7583        }
7584
7585        let p = CscMatrix::new_from_triplets(n, n, p_rows.clone(), p_cols.clone(), p_vals.clone());
7586
7587        // --- Build q (linear cost) ---
7588        let mut q_vec = vec![0.0; n];
7589        for t in 1..=T {
7590            let w = if t == T { qf_diag } else { q_diag };
7591            for k in 0..nx {
7592                // cost = w[k] * (x[k] - xref[k])^2 = w[k]*x[k]^2 - 2*w[k]*xref[k]*x[k] + const
7593                // q contribution: -2*w[k]*xref[k] (but clarabel uses 0.5*z'Pz + q'z, so just -2*w*xref)
7594                q_vec[x_idx(t, k)] = -w[k] * 2.0 * xref[t][k];
7595            }
7596        }
7597
7598        // --- Build constraints ---
7599        // Equality: dynamics x[t+1] = A[t]*x[t] + B[t]*u[t] + C[t]  → A[t]*x[t] + B[t]*u[t] - x[t+1] + C[t] = 0
7600        // Equality: x[0] = x0
7601        // Box constraints on u and x
7602
7603        let n_eq = nx * (T + 1); // T dynamics + 1 initial = (T+1)*nx = 24
7604        let n_steer_rate = T - 1; // |δ[t+1]-δ[t]| <= MAX_DSTEER*DT → 2*(T-1) ineq
7605        let n_u_box = 2 * nu * T; // upper+lower for each u component: 20
7606        let n_steer_rate_box = 2 * n_steer_rate; // 8
7607        let n_v_box = 2 * (T + 1); // speed constraints: 12
7608        let n_ineq = n_u_box + n_steer_rate_box + n_v_box;
7609
7610        let mut a_rows = Vec::new();
7611        let mut a_cols = Vec::new();
7612        let mut a_vals = Vec::new();
7613        let mut b_vec = vec![0.0; n_eq + n_ineq];
7614
7615        // -- Equality: x[0] = x0 --
7616        let x0 = state.to_vector();
7617        for k in 0..nx {
7618            let row = k;
7619            a_rows.push(row);
7620            a_cols.push(x_idx(0, k));
7621            a_vals.push(1.0);
7622            b_vec[row] = x0[k];
7623        }
7624
7625        // -- Equality: dynamics for t=0..T-1 --
7626        for t in 0..T {
7627            let (a_mat, b_mat, c_vec) = get_linear_model_matrix(xbar[t][2], xbar[t][3], 0.0);
7628            let base_row = nx * (t + 1);
7629
7630            // x[t+1] = A*x[t] + B*u[t] + C
7631            // → -x[t+1] + A*x[t] + B*u[t] = -C
7632            for i in 0..nx {
7633                let row = base_row + i;
7634                // -x[t+1][i]
7635                a_rows.push(row);
7636                a_cols.push(x_idx(t + 1, i));
7637                a_vals.push(-1.0);
7638                // A[i,:] * x[t]
7639                for j in 0..nx {
7640                    let val = a_mat[(i, j)];
7641                    if val.abs() > 1e-15 {
7642                        a_rows.push(row);
7643                        a_cols.push(x_idx(t, j));
7644                        a_vals.push(val);
7645                    }
7646                }
7647                // B[i,:] * u[t]
7648                for j in 0..nu {
7649                    let val = b_mat[(i, j)];
7650                    if val.abs() > 1e-15 {
7651                        a_rows.push(row);
7652                        a_cols.push(u_idx(t, j));
7653                        a_vals.push(val);
7654                    }
7655                }
7656                b_vec[row] = -c_vec[i];
7657            }
7658        }
7659
7660        // -- Inequality constraints (as cones) --
7661        // clarabel inequality: A*z + s = b, s >= 0  → A*z <= b
7662        let ineq_offset = n_eq;
7663        let mut ineq_row = 0;
7664
7665        // u box: -MAX_ACCEL <= a[t] <= MAX_ACCEL, -MAX_STEER <= δ[t] <= MAX_STEER
7666        let u_bounds = [MAX_ACCEL, MAX_STEER];
7667        for t in 0..T {
7668            for (k, &bound) in u_bounds.iter().enumerate() {
7669                // u[t][k] <= bound
7670                let row = ineq_offset + ineq_row;
7671                a_rows.push(row);
7672                a_cols.push(u_idx(t, k));
7673                a_vals.push(1.0);
7674                b_vec[row] = bound;
7675                ineq_row += 1;
7676
7677                // -u[t][k] <= bound
7678                let row = ineq_offset + ineq_row;
7679                a_rows.push(row);
7680                a_cols.push(u_idx(t, k));
7681                a_vals.push(-1.0);
7682                b_vec[row] = bound;
7683                ineq_row += 1;
7684            }
7685        }
7686
7687        // Steer rate: |δ[t+1] - δ[t]| <= MAX_DSTEER * DT
7688        let max_steer_delta = MAX_DSTEER * DT;
7689        for t in 0..(T - 1) {
7690            // δ[t+1] - δ[t] <= max_steer_delta
7691            let row = ineq_offset + ineq_row;
7692            a_rows.push(row);
7693            a_cols.push(u_idx(t + 1, 1));
7694            a_vals.push(1.0);
7695            a_rows.push(row);
7696            a_cols.push(u_idx(t, 1));
7697            a_vals.push(-1.0);
7698            b_vec[row] = max_steer_delta;
7699            ineq_row += 1;
7700
7701            // -(δ[t+1] - δ[t]) <= max_steer_delta
7702            let row = ineq_offset + ineq_row;
7703            a_rows.push(row);
7704            a_cols.push(u_idx(t + 1, 1));
7705            a_vals.push(-1.0);
7706            a_rows.push(row);
7707            a_cols.push(u_idx(t, 1));
7708            a_vals.push(1.0);
7709            b_vec[row] = max_steer_delta;
7710            ineq_row += 1;
7711        }
7712
7713        // Speed box: MIN_SPEED <= x[2,t] <= MAX_SPEED for all t
7714        for t in 0..=T {
7715            // x[2,t] <= MAX_SPEED
7716            let row = ineq_offset + ineq_row;
7717            a_rows.push(row);
7718            a_cols.push(x_idx(t, 2));
7719            a_vals.push(1.0);
7720            b_vec[row] = MAX_SPEED;
7721            ineq_row += 1;
7722
7723            // -x[2,t] <= -MIN_SPEED
7724            let row = ineq_offset + ineq_row;
7725            a_rows.push(row);
7726            a_cols.push(x_idx(t, 2));
7727            a_vals.push(-1.0);
7728            b_vec[row] = -MIN_SPEED;
7729            ineq_row += 1;
7730        }
7731
7732        let m = n_eq + n_ineq;
7733
7734        // Build CscMatrix for A (constraint matrix)
7735        // clarabel expects column-major CscMatrix
7736        let a_csc = CscMatrix::new_from_triplets(m, n, a_rows, a_cols, a_vals);
7737
7738        // Cone specification
7739        let cones = vec![
7740            SupportedConeT::ZeroConeT(n_eq),          // equality constraints
7741            SupportedConeT::NonnegativeConeT(n_ineq), // inequality constraints (s >= 0)
7742        ];
7743
7744        let settings = DefaultSettingsBuilder::default()
7745            .verbose(false)
7746            .build()
7747            .unwrap();
7748
7749        let mut solver = DefaultSolver::new(&p, &q_vec, &a_csc, &b_vec, &cones, settings).ok()?;
7750        solver.solve();
7751
7752        if solver.solution.status != SolverStatus::Solved {
7753            return None;
7754        }
7755
7756        let z = &solver.solution.x;
7757
7758        let controls: Vec<Vector2<f64>> = (0..T)
7759            .map(|t| Vector2::new(z[u_idx(t, 0)], z[u_idx(t, 1)]))
7760            .collect();
7761
7762        let predicted: Vec<Vector4<f64>> = (0..=T)
7763            .map(|t| {
7764                Vector4::new(
7765                    z[x_idx(t, 0)],
7766                    z[x_idx(t, 1)],
7767                    z[x_idx(t, 2)],
7768                    z[x_idx(t, 3)],
7769                )
7770            })
7771            .collect();
7772
7773        Some((controls, predicted))
7774    }
7775
7776    /// Wrapper for QP solver matching the test optimizer signature.
7777    fn optimize_linearized_controls_qp(
7778        xref: &[Vector4<f64>],
7779        xbar: &[Vector4<f64>],
7780        state: &State,
7781        _initial_controls: &[Vector2<f64>],
7782    ) -> Vec<Vector2<f64>> {
7783        if let Some((controls, _)) = solve_mpc_qp_clarabel(xref, xbar, state) {
7784            controls
7785        } else {
7786            // Fallback to projected-gradient if QP fails
7787            optimize_linearized_controls(xref, xbar, state, _initial_controls)
7788        }
7789    }
7790
7791    #[test]
7792    #[ignore = "long-running regression scenario"]
7793    fn test_mpc_qp_solver_matches_projected_gradient_on_representative_steps() {
7794        let default_trace = collect_switch_back_detailed_trace_with_test_optimizer(
7795            80,
7796            optimize_linearized_controls,
7797        );
7798        let default_warm_start = collect_switch_back_warm_start_trace_with_test_optimizer(
7799            80,
7800            optimize_linearized_controls,
7801        );
7802
7803        println!("=== QP Solver (clarabel) vs Projected-Gradient ===");
7804        println!();
7805
7806        for &step in &[20usize, 70, 75, 78] {
7807            let row = detailed_row_at_step(&default_trace, step);
7808            let ws = default_warm_start.get(&step).unwrap();
7809            let state = state_from_vector4(&row.state);
7810
7811            // Get xbar for QP (same as projected-gradient uses)
7812            let mut controls_for_xbar = ws.to_vec();
7813            controls_for_xbar.resize(T, Vector2::zeros());
7814            apply_control_constraints(&mut controls_for_xbar);
7815            let xbar = predict_motion(state, &controls_for_xbar);
7816
7817            // Projected-gradient result (1 outer iteration for fair comparison)
7818            let pg_controls = optimize_linearized_controls(&row.xref, &xbar, &state, ws);
7819
7820            // QP result
7821            let qp_result = solve_mpc_qp_clarabel(&row.xref, &xbar, &state);
7822
7823            if let Some((qp_controls, _qp_predicted)) = qp_result {
7824                let control_diff = summarize_vec2_sequence_diff(
7825                    &qp_controls,
7826                    &pg_controls.iter().map(|c| [c[0], c[1]]).collect::<Vec<_>>(),
7827                );
7828                println!(
7829                    "step {step}: control_diff=[{:.6e}, {:.6e}] pg_steer={:.6} qp_steer={:.6}",
7830                    control_diff[0], control_diff[1], pg_controls[0][1], qp_controls[0][1],
7831                );
7832            } else {
7833                println!("step {step}: QP solver failed!");
7834            }
7835        }
7836    }
7837
7838    #[test]
7839    #[ignore = "long-running regression scenario"]
7840    fn test_mpc_qp_solver_closed_loop_parity() {
7841        let python_final_state = [
7842            -0.014_888_855_512_389,
7843            0.698_023_990_780_443,
7844            0.132_179_560_675_735,
7845            1.589_745_981_585_03,
7846        ];
7847        let python_history_length = 347usize;
7848
7849        let default_result = run_mpc_simulation();
7850        let default_final = default_result.final_state.to_vector();
7851
7852        // QP solver closed-loop
7853        let (cx, cy, cyaw) = generate_switch_back_course();
7854        let speed_profile = calc_speed_profile(&cx, &cy, &cyaw, TARGET_SPEED);
7855
7856        let mut state = State::new(cx[0], cy[0], 0.0, cyaw[0]);
7857        if state.yaw - cyaw[0] >= PI {
7858            state.yaw -= 2.0 * PI;
7859        } else if state.yaw - cyaw[0] <= -PI {
7860            state.yaw += 2.0 * PI;
7861        }
7862
7863        let final_index = cx.len() - 1;
7864        let goal = (cx[final_index], cy[final_index]);
7865        let mut target_index = 0;
7866        let mut warm_start = vec![Vector2::zeros(); T];
7867        let mut step_count = 0;
7868
7869        for _ in 0..MAX_SIM_STEPS {
7870            let (xref, new_index) = calc_ref_trajectory(
7871                &state,
7872                &cx,
7873                &cy,
7874                &cyaw,
7875                &speed_profile,
7876                UPSTREAM_SWITCH_BACK_TICK,
7877                target_index,
7878            );
7879            target_index = new_index;
7880
7881            let result = iterative_linear_mpc_control_with_test_optimizer(
7882                &xref,
7883                &state,
7884                &warm_start,
7885                optimize_linearized_controls_qp,
7886            );
7887
7888            state.update(result.controls[0][0], result.controls[0][1]);
7889            warm_start = next_warm_start(&result.controls);
7890            step_count += 1;
7891
7892            if check_goal(&state, goal, target_index, final_index) {
7893                break;
7894            }
7895        }
7896
7897        let qp_final = state.to_vector();
7898        let qp_len = step_count + 1;
7899
7900        let default_diffs: Vec<f64> = (0..4)
7901            .map(|i| (default_final[i] - python_final_state[i]).abs())
7902            .collect();
7903        let qp_diffs: Vec<f64> = (0..4)
7904            .map(|i| (qp_final[i] - python_final_state[i]).abs())
7905            .collect();
7906
7907        println!("=== Closed-Loop Parity: PG vs QP (clarabel) ===");
7908        println!(
7909            "Python ref: {:?} (len={})",
7910            python_final_state, python_history_length
7911        );
7912        println!();
7913        println!(
7914            "PG: [{:.6},{:.6},{:.6},{:.6}] len={} diffs=[{:.4e},{:.4e},{:.4e},{:.4e}]",
7915            default_final[0],
7916            default_final[1],
7917            default_final[2],
7918            default_final[3],
7919            default_result.hist_x.len(),
7920            default_diffs[0],
7921            default_diffs[1],
7922            default_diffs[2],
7923            default_diffs[3]
7924        );
7925        println!(
7926            "QP: [{:.6},{:.6},{:.6},{:.6}] len={} diffs=[{:.4e},{:.4e},{:.4e},{:.4e}]",
7927            qp_final[0],
7928            qp_final[1],
7929            qp_final[2],
7930            qp_final[3],
7931            qp_len,
7932            qp_diffs[0],
7933            qp_diffs[1],
7934            qp_diffs[2],
7935            qp_diffs[3]
7936        );
7937
7938        let pg_total: f64 = default_diffs.iter().sum();
7939        let qp_total: f64 = qp_diffs.iter().sum();
7940        println!(
7941            "Total: PG={:.4e} QP={:.4e} improvement={:.1}%",
7942            pg_total,
7943            qp_total,
7944            if pg_total > 0.0 {
7945                (1.0 - qp_total / pg_total) * 100.0
7946            } else {
7947                0.0
7948            }
7949        );
7950    }
7951
7952    #[test]
7953    #[ignore = "long-running regression scenario"]
7954    fn test_mpc_solver_comparison_qp_vs_pg_detailed() {
7955        let expected_trace =
7956            parse_switch_back_trace(include_str!("testdata/mpc_switch_back_prefix_trace.csv"));
7957
7958        // 4 solvers
7959        let hybrid_trace = collect_switch_back_detailed_trace(350); // production = hybrid
7960        let qp_trace = collect_switch_back_detailed_trace_with_test_optimizer(
7961            350,
7962            optimize_linearized_controls_qp,
7963        );
7964        let pg_trace = collect_switch_back_detailed_trace_with_test_optimizer(
7965            350,
7966            optimize_linearized_controls_with_full_candidate_expansion,
7967        );
7968        let pg_orig_trace = collect_switch_back_detailed_trace_with_test_optimizer(
7969            350,
7970            optimize_linearized_controls_original,
7971        );
7972
7973        println!("=== Solver Comparison (state diff sum vs Python) ===");
7974        println!(
7975            "{:>5} {:>10} {:>10} {:>10} {:>10}  best",
7976            "step", "hybrid", "qp-only", "pg-exp", "pg-orig"
7977        );
7978
7979        let check_steps: Vec<usize> = (0..=80)
7980            .step_by(5)
7981            .chain([71, 72, 73, 74, 75, 76, 77, 78, 79, 80])
7982            .chain((85..350).step_by(10))
7983            .collect();
7984
7985        for &step in &check_steps {
7986            if step >= expected_trace.len() {
7987                continue;
7988            }
7989            let py = &expected_trace[step];
7990            let hy = hybrid_trace.iter().find(|r| r.step == step);
7991            let qp = qp_trace.iter().find(|r| r.step == step);
7992            let pg = pg_trace.iter().find(|r| r.step == step);
7993            let po = pg_orig_trace.iter().find(|r| r.step == step);
7994
7995            if let (Some(hy), Some(qp), Some(pg), Some(po)) = (hy, qp, pg, po) {
7996                let hy_s: f64 = trace_state_diff(&hy.state, &py.state).iter().sum();
7997                let qp_s: f64 = trace_state_diff(&qp.state, &py.state).iter().sum();
7998                let pg_s: f64 = trace_state_diff(&pg.state, &py.state).iter().sum();
7999                let po_s: f64 = trace_state_diff(&po.state, &py.state).iter().sum();
8000
8001                let min_s = hy_s.min(qp_s).min(pg_s).min(po_s);
8002                let best = if (hy_s - min_s).abs() < 1e-15 {
8003                    "HYBRID"
8004                } else if (qp_s - min_s).abs() < 1e-15 {
8005                    "QP"
8006                } else if (pg_s - min_s).abs() < 1e-15 {
8007                    "PG-exp"
8008                } else {
8009                    "PG-orig"
8010                };
8011
8012                println!(
8013                    "{:5} {:10.4e} {:10.4e} {:10.4e} {:10.4e}  {}",
8014                    step, hy_s, qp_s, pg_s, po_s, best
8015                );
8016            }
8017        }
8018
8019        // Win count
8020        let mut wins = [0u32; 4]; // hybrid, qp, pg-exp, pg-orig
8021        let labels = ["HYBRID", "QP-only", "PG-expanded", "PG-original"];
8022        let min_steps = [
8023            hybrid_trace.len(),
8024            qp_trace.len(),
8025            pg_trace.len(),
8026            pg_orig_trace.len(),
8027            expected_trace.len(),
8028        ]
8029        .into_iter()
8030        .min()
8031        .unwrap();
8032        for (step, py) in expected_trace.iter().enumerate().take(min_steps) {
8033            let traces = [&hybrid_trace, &qp_trace, &pg_trace, &pg_orig_trace];
8034            let diffs: Vec<f64> = traces
8035                .iter()
8036                .map(|t| {
8037                    t.iter()
8038                        .find(|r| r.step == step)
8039                        .map(|r| trace_state_diff(&r.state, &py.state).iter().sum())
8040                        .unwrap_or(f64::INFINITY)
8041                })
8042                .collect();
8043            let min_d = diffs.iter().copied().fold(f64::INFINITY, f64::min);
8044            for (i, &d) in diffs.iter().enumerate() {
8045                if (d - min_d).abs() < 1e-15 {
8046                    wins[i] += 1;
8047                    break;
8048                }
8049            }
8050        }
8051        println!("\n=== Win count (closest to Python per step, out of {min_steps}) ===");
8052        for (i, label) in labels.iter().enumerate() {
8053            println!("{:12}: {} steps", label, wins[i]);
8054        }
8055    }
8056
8057    #[test]
8058    #[ignore = "long-running regression scenario"]
8059    fn test_mpc_switch_back_characterizes_v_injection_pull_ratio_threshold() {
8060        let default_trace = collect_switch_back_detailed_trace_with_test_optimizer(
8061            330,
8062            optimize_linearized_controls,
8063        );
8064        let expanded_trace = collect_switch_back_detailed_trace_with_test_optimizer(
8065            330,
8066            optimize_linearized_controls_with_full_candidate_expansion,
8067        );
8068        let default_warm_start = collect_switch_back_warm_start_trace_with_test_optimizer(
8069            330,
8070            optimize_linearized_controls,
8071        );
8072
8073        let d325 = detailed_row_at_step(&default_trace, 325);
8074        let e325 = detailed_row_at_step(&expanded_trace, 325);
8075        let e326 = detailed_row_at_step(&expanded_trace, 326);
8076        let ws = default_warm_start.get(&325).unwrap();
8077
8078        // Baseline values
8079        let d_v = d325.state[2]; // default v at step 325
8080        let e325_v = e325.state[2]; // expanded v at step 325: ~ -0.804
8081        let e326_v = e326.state[2]; // expanded v at step 326: ~ -0.604
8082
8083        let d_baseline_controls: Vec<[f64; 2]> =
8084            d325.controls.iter().map(|v| [v[0], v[1]]).collect();
8085        let e_baseline_controls: Vec<[f64; 2]> =
8086            e325.controls.iter().map(|v| [v[0], v[1]]).collect();
8087
8088        println!("=== V-Injection Pull Ratio Threshold Sweep ===");
8089        println!(
8090            "default v={:.6}, e325_v={:.6}, e326_v={:.6}",
8091            d_v, e325_v, e326_v
8092        );
8093        println!("Sweeping injected v from e325_v to e326_v in 0.02 steps");
8094        println!();
8095        println!(
8096            "{:>10} {:>10} {:>10} {:>10} {:>10}",
8097            "inj_v", "steer", "same_c0", "opp_c0", "ratio"
8098        );
8099
8100        // Sweep from e325_v to e326_v
8101        let n_steps = 11;
8102        let mut prev_ratio = f64::INFINITY;
8103        let mut threshold_v = None;
8104
8105        for i in 0..=n_steps {
8106            let frac = i as f64 / n_steps as f64;
8107            let injected_v = e325_v + frac * (e326_v - e325_v);
8108
8109            // Create hybrid state: default state with injected v
8110            let mut hybrid = d325.state;
8111            hybrid[2] = injected_v;
8112            let hybrid_state = state_from_vector4(&hybrid);
8113
8114            let result = iterative_linear_mpc_control_with_test_optimizer(
8115                &d325.xref,
8116                &hybrid_state,
8117                ws,
8118                optimize_linearized_controls,
8119            );
8120
8121            let same_c0 = summarize_vec2_sequence_diff(&result.controls, &d_baseline_controls)[0];
8122            let opp_c0 = summarize_vec2_sequence_diff(&result.controls, &e_baseline_controls)[0];
8123
8124            let ratio = if opp_c0 > 0.0 {
8125                same_c0 / opp_c0
8126            } else {
8127                f64::INFINITY
8128            };
8129
8130            println!(
8131                "{:10.6} {:10.6} {:10.4e} {:10.4e} {:10.3}",
8132                injected_v, result.controls[0][1], same_c0, opp_c0, ratio
8133            );
8134
8135            // Detect threshold crossing (ratio drops below 4.0)
8136            if threshold_v.is_none() && prev_ratio > 4.0 && ratio <= 4.0 {
8137                threshold_v = Some((injected_v, ratio));
8138            }
8139            prev_ratio = ratio;
8140        }
8141
8142        if let Some((v, r)) = threshold_v {
8143            println!(
8144                "\nThreshold: ratio drops below 4.0 at v={:.6} (ratio={:.3})",
8145                v, r
8146            );
8147        } else {
8148            println!("\nNo threshold crossing found in this range");
8149        }
8150    }
8151}