Skip to main content

copp\copp\copp2\dp2/
reach_set2.rs

1//! Reachable-set construction for second-order path parameterization.
2//!
3//! # Method identity
4//! This module implements the reachable-set stage via **Reachability Analysis (RA)** in
5//! a **Dynamic Programming (DP)** compatible form, which can be used by:
6//! - **Time-Optimal Path Parameterization (TOPP2)**,
7//! - **Convex-Objective Path Parameterization (COPP2)**.
8//!
9//! # Discrete variables (local notation)
10//! On a path grid `s[0..=n]`:
11//! - `a[k]` denotes $\dot{s}_k^2$ (nonnegative scalar state);
12//! - reachable interval at station `k` is `[a_min[k], a_max[k]]`.
13//!
14//! # High-level pipeline
15//! 1. Validate boundary feasibility at both interval ends.
16//! 2. Backward pass from terminal boundary to construct feasible intervals.
17//! 3. Optional forward clipping (when bidirectional mode is enabled) to enforce start boundary.
18//! 4. Return interval arrays `a_min` / `a_max` for downstream solvers.
19
20use crate::copp::copp2::formulation::Topp2Problem;
21use crate::copp::{ApproxOrdering, approx_order};
22use crate::diag::{
23    CoppError, DebugVerboser, SilentVerboser, SummaryVerboser, TraceVerboser, Verboser, Verbosity,
24    check_abs_rel_tol, check_strictly_positive, format_duration_human,
25};
26use crate::math::numerical::{
27    LP_BOUND, Lp2dWarmStart, LpToleranceOptions, lp_1d, lp_2d_incre_max_y, normalize_lp2d,
28};
29use core::f64;
30
31/// Reachable intervals of $a(s)=\dot{s}^2$ for TOPP2/COPP2.
32/// For each point `s[k]`, the reachable set is `a_min[k] <= a[k] <= a_max[k]`.
33pub struct ReachSet2 {
34    /// Upper reachable bound `a_max[k]` at each station.
35    pub a_max: Vec<f64>,
36    /// Lower reachable bound `a_min[k]` at each station.
37    pub a_min: Vec<f64>,
38}
39
40/// Compute backward-only reachable intervals of $a(s)=\dot{s}^2$ using Reachability Analysis.
41///
42/// Performs a single backward pass from the terminal boundary; the start boundary
43/// is **not** enforced.  Use this when only the terminal state is constrained, or
44/// as an intermediate step before bidirectional analysis.
45///
46/// # Returns
47/// [`ReachSet2`](crate::solver::reach_set2::ReachSet2) with `a_min[k] <= a[k] <= a_max[k]` for every station.
48///
49/// # Errors
50/// Returns [`CoppError`](crate::diag::CoppError) when boundary states are infeasible, LP subproblems fail,
51/// or numerical comparisons violate configured tolerances.
52///
53/// # Contract
54/// - `problem` indices and boundaries must be consistent with the constraints domain.
55/// - `options` tolerances must be positive and numerically meaningful.
56#[inline]
57pub fn reach_set2_backward(
58    problem: &Topp2Problem,
59    options: &ReachSet2Options,
60) -> Result<ReachSet2, CoppError> {
61    match options.verbosity {
62        Verbosity::Silent => reach_set2_core::<false>(problem, (options, SilentVerboser)),
63        Verbosity::Summary => reach_set2_core::<false>(problem, (options, SummaryVerboser::new())),
64        Verbosity::Debug => reach_set2_core::<false>(problem, (options, DebugVerboser::new())),
65        Verbosity::Trace => reach_set2_core::<false>(problem, (options, TraceVerboser::new())),
66    }
67}
68
69/// Compute bidirectional reachable intervals of $a(s)=\dot{s}^2$.
70///
71/// Performs a backward pass then clips the result with a forward pass to enforce
72/// **both** the start and terminal boundary constraints simultaneously.
73///
74/// # Returns
75/// [`ReachSet2`](crate::solver::reach_set2::ReachSet2) with `a_min[k] <= a[k] <= a_max[k]` for every station.
76///
77/// # Errors
78/// Returns [`CoppError`](crate::diag::CoppError) when boundary states are infeasible, LP subproblems fail,
79/// or numerical comparisons violate configured tolerances.
80///
81/// # Contract
82/// - `problem` indices and boundaries must be consistent with the constraints domain.
83/// - `options` tolerances must be positive and numerically meaningful.
84#[inline]
85pub fn reach_set2_bidirectional(
86    problem: &Topp2Problem,
87    options: &ReachSet2Options,
88) -> Result<ReachSet2, CoppError> {
89    match options.verbosity {
90        Verbosity::Silent => reach_set2_core::<true>(problem, (options, SilentVerboser)),
91        Verbosity::Summary => reach_set2_core::<true>(problem, (options, SummaryVerboser::new())),
92        Verbosity::Debug => reach_set2_core::<true>(problem, (options, DebugVerboser::new())),
93        Verbosity::Trace => reach_set2_core::<true>(problem, (options, TraceVerboser::new())),
94    }
95}
96
97/// Core RA implementation with layered verbosity.
98fn reach_set2_core<const BIDIRECTION: bool>(
99    problem: &Topp2Problem,
100    options_verboser: (&ReachSet2Options, impl Verboser),
101) -> Result<ReachSet2, CoppError> {
102    let (options, mut verboser) = options_verboser;
103    if verboser.is_enabled(Verbosity::Summary) {
104        verboser.record_start_time();
105        crate::verbosity_log!(
106            crate::diag::Verbosity::Summary,
107            "reach_set2 started: {} <= idx_s <= {}, a_start = {}, a_final = {}.",
108            problem.idx_s_interval.0,
109            problem.idx_s_interval.1,
110            problem.a_boundary.0,
111            problem.a_boundary.1,
112        );
113        if BIDIRECTION {
114            crate::verbosity_log!(
115                crate::diag::Verbosity::Summary,
116                "Bidirectional reachable set will be computed."
117            );
118        } else {
119            crate::verbosity_log!(
120                crate::diag::Verbosity::Summary,
121                "Backward reachable set will be computed."
122            );
123        }
124    }
125
126    let (idx_s_start, idx_s_final) = problem.idx_s_interval;
127    let a_max_0 = problem.constraints.amax_unchecked(problem.idx_s_interval.0);
128    if matches!(
129        approx_order(
130            problem.a_boundary.0,
131            a_max_0,
132            options.a_cmp_abs_tol,
133            options.a_cmp_rel_tol,
134        ),
135        ApproxOrdering::Greater
136    ) {
137        let err = CoppError::Infeasible(
138            "reach_set2".into(),
139            format!(
140                "The initial state a_start = {} cannot be greater than the maximum feasible state a_max[0] = {a_max_0} at idx_s_start = {idx_s_start}",
141                problem.a_boundary.0,
142            ),
143        );
144        if verboser.is_enabled(Verbosity::Debug) {
145            crate::verbosity_log!(crate::diag::Verbosity::Summary, "{err:?}");
146        } else if verboser.is_enabled(Verbosity::Summary) {
147            crate::verbosity_log!(
148                crate::diag::Verbosity::Debug,
149                "reach_set2: the backward pass failed at index {idx_s_start} due to infeasibility of the initial state."
150            );
151        }
152        return Err(err);
153    }
154    let a_max_f = problem.constraints.amax_unchecked(problem.idx_s_interval.1);
155    if matches!(
156        approx_order(
157            problem.a_boundary.1,
158            a_max_f,
159            options.a_cmp_abs_tol,
160            options.a_cmp_rel_tol,
161        ),
162        ApproxOrdering::Greater
163    ) {
164        let err = CoppError::Infeasible(
165            "reach_set2".into(),
166            format!(
167                "The final state a_final = {} cannot be greater than the maximum feasible state a_max[n] = {a_max_f} at idx_s_final = {idx_s_final}",
168                problem.a_boundary.1,
169            ),
170        );
171        if verboser.is_enabled(Verbosity::Debug) {
172            crate::verbosity_log!(crate::diag::Verbosity::Summary, "{err:?}");
173        } else if verboser.is_enabled(Verbosity::Summary) {
174            crate::verbosity_log!(
175                crate::diag::Verbosity::Debug,
176                "reach_set2: the backward pass failed at index {idx_s_final} due to infeasibility of the final state."
177            );
178        }
179        return Err(err);
180    }
181
182    // Step 1. Initialize a_max and a_min at s_final
183    let n = idx_s_final - idx_s_start;
184    let mut a_max = vec![f64::INFINITY; n + 1];
185    let mut a_min = vec![0.0; n + 1];
186    *a_max.last_mut().unwrap() = problem.a_boundary.1;
187    *a_min.last_mut().unwrap() = problem.a_boundary.1;
188    // Step 2. Backward pass
189    if verboser.is_enabled(Verbosity::Debug) {
190        crate::verbosity_log!(crate::diag::Verbosity::Summary, "Backward pass started.");
191    }
192
193    let mut a_b = Vec::<(f64, f64, f64)>::with_capacity(2 + 2 * problem.constraints.acc_rows());
194    let mut a_max_next = problem.a_boundary.1;
195    let mut a_min_next = problem.a_boundary.1;
196    for (k, (a_max_k, a_min_k)) in a_max
197        .iter_mut()
198        .zip(a_min.iter_mut())
199        .take(n)
200        .enumerate()
201        .rev()
202    {
203        let idx_s = idx_s_start + k;
204        if verboser.is_enabled(Verbosity::Trace) {
205            crate::verbosity_log!(
206                crate::diag::Verbosity::Summary,
207                "\tBackward pass at k = {k} (idx_s = {idx_s}) to compute a[k]: {a_min_next} <= a[k+1] <= {a_max_next}."
208            );
209        }
210
211        a_b.clear();
212        a_b.push((1.0, 0.0, a_max_next));
213        a_b.push((-1.0, 0.0, -a_min_next));
214        problem.constraints.fill_acc_topp2::<true>(&mut a_b, idx_s);
215        // a_b.0 * a[k+1] + a_b.1 * a[k] <= a_b.2
216
217        let a_next_mid = 0.5 * (a_max_next + a_min_next);
218        let (a_max_curr, a_min_curr) = match approx_order(
219            a_max_next,
220            a_min_next,
221            options.a_cmp_abs_tol,
222            options.a_cmp_rel_tol,
223        ) {
224            ApproxOrdering::Equal => {
225                if verboser.is_enabled(Verbosity::Trace) {
226                    crate::verbosity_log!(
227                        crate::diag::Verbosity::Summary,
228                        "\t\ta[k+1] = {a_next_mid}"
229                    );
230                }
231                lp_1d::<true>(
232                    a_b.iter().skip(2).map(|&coeffs| {
233                        // coeffs.0 * a_next  + coeffs.1* a_curr <= coeffs.2
234                        // coeffs.1 * a_curr <= coeffs.2 - coeffs.0 * a_next
235                        (coeffs.1, coeffs.2 - coeffs.0 * a_next_mid)
236                    }),
237                    &LpToleranceOptions::with_feas_tol(options.lp_feas_tol),
238                )
239            }
240            ApproxOrdering::Greater => {
241                if verboser.is_enabled(Verbosity::Trace) {
242                    crate::verbosity_log!(
243                        crate::diag::Verbosity::Summary,
244                        "\t\t{a_min_next} <= a[k+1] <= {a_max_next}"
245                    );
246                }
247                // Check whether the forward pass of `amin_curr` can be skipped
248                let (a_test_max, a_test_min) = lp_1d::<true>(
249                    a_b.iter().skip(2).map(|&coeffs| {
250                        // coeffs.0 * a_next  + coeffs.1* a_curr <= coeffs.2
251                        // coeffs.0 * a_next <= coeffs.2 - coeffs.1 * a_curr
252                        (coeffs.0, coeffs.2 - coeffs.1 * *a_min_k)
253                    }),
254                    &LpToleranceOptions::with_feas_tol(options.lp_feas_tol),
255                );
256                let flag_need_min = a_test_max.is_nan()
257                    || a_test_min.is_nan()
258                    || a_test_max < a_min_next
259                    || a_test_max > a_max_next;
260                if verboser.is_enabled(Verbosity::Trace) {
261                    crate::verbosity_log!(
262                        crate::diag::Verbosity::Summary,
263                        "\t\tBackward skip checks at idx_s = {idx_s}: need_max = true, need_min = {flag_need_min}."
264                    );
265                }
266                if flag_need_min {
267                    backward_bound_a_next::<true, true>(&mut a_b, a_next_mid, options.lp_feas_tol)
268                } else {
269                    (
270                        backward_bound_a_next::<true, false>(
271                            &mut a_b,
272                            a_next_mid,
273                            options.lp_feas_tol,
274                        )
275                        .0,
276                        *a_min_k,
277                    )
278                }
279            }
280            ApproxOrdering::Less => {
281                let err = CoppError::Infeasible(
282                    "reach_set2".into(),
283                    format!(
284                        "The reachable set is empty at index {idx_s} during the backward pass where a_max_next = {a_max_next}, a_min_next = {a_min_next}"
285                    ),
286                );
287                if verboser.is_enabled(Verbosity::Debug) {
288                    crate::verbosity_log!(crate::diag::Verbosity::Summary, "{err:?}");
289                } else if verboser.is_enabled(Verbosity::Summary) {
290                    crate::verbosity_log!(
291                        crate::diag::Verbosity::Debug,
292                        "reach_set2: the backward pass failed at index {idx_s} due to infeasibility."
293                    );
294                }
295                return Err(err);
296            }
297        };
298
299        if a_max_curr.is_nan() || a_min_curr.is_nan() {
300            let err = CoppError::Infeasible(
301                "reach_set2".into(),
302                format!(
303                    "The reachable set is empty at index {idx_s} during the backward pass where a_max = {a_max_curr}, a_min = {a_min_curr}"
304                ),
305            );
306            if verboser.is_enabled(Verbosity::Debug) {
307                crate::verbosity_log!(crate::diag::Verbosity::Summary, "{err:?}");
308            } else if verboser.is_enabled(Verbosity::Summary) {
309                crate::verbosity_log!(
310                    crate::diag::Verbosity::Debug,
311                    "reach_set2: the backward pass failed at index {idx_s} due to infeasibility."
312                );
313            }
314            return Err(err);
315        }
316        if verboser.is_enabled(Verbosity::Trace) {
317            crate::verbosity_log!(
318                crate::diag::Verbosity::Summary,
319                "\t\tBackward LP result at k = {k}: {a_min_curr} <= a[k] <= {a_max_curr}."
320            );
321        }
322
323        *a_max_k = a_max_curr.min(problem.constraints.amax_unchecked(idx_s));
324        *a_min_k = a_min_curr.max(0.0);
325
326        if verboser.is_enabled(Verbosity::Trace) {
327            crate::verbosity_log!(
328                crate::diag::Verbosity::Summary,
329                "\t\tAfter clipping with path constraints at k = {k}: {} <= a[k] <= {}.",
330                *a_min_k,
331                *a_max_k
332            );
333        }
334
335        match approx_order(
336            *a_max_k,
337            *a_min_k,
338            options.a_cmp_abs_tol,
339            options.a_cmp_rel_tol,
340        ) {
341            ApproxOrdering::Less => {
342                let err = CoppError::Infeasible(
343                    "reach_set2".into(),
344                    format!(
345                        "The reachable set is empty at k = {k} (idx_s = {idx_s}) during the backward pass where a_max = {a_max_k}, a_min = {a_min_k}"
346                    ),
347                );
348                if verboser.is_enabled(Verbosity::Debug) {
349                    crate::verbosity_log!(crate::diag::Verbosity::Summary, "{err:?}");
350                } else if verboser.is_enabled(Verbosity::Summary) {
351                    crate::verbosity_log!(
352                        crate::diag::Verbosity::Debug,
353                        "reach_set2: the backward pass failed at k = {k} (idx_s = {idx_s}) due to infeasibility."
354                    );
355                }
356                return Err(err);
357            }
358            ApproxOrdering::Equal => {
359                if verboser.is_enabled(Verbosity::Debug) && k < n - 1 {
360                    crate::verbosity_log!(
361                        crate::diag::Verbosity::Summary,
362                        "The backward reachable set at k = {k} (idx_s = {idx_s}) is degenerate since a_max = {a_max_k} and a_min = {a_min_k} are approximately equal."
363                    );
364                }
365                *a_max_k = 0.5 * (*a_max_k + *a_min_k);
366                *a_min_k = *a_max_k;
367            }
368            ApproxOrdering::Greater => {}
369        }
370        a_max_next = *a_max_k;
371        a_min_next = *a_min_k;
372
373        if verboser.is_enabled(Verbosity::Trace) {
374            crate::verbosity_log!(
375                crate::diag::Verbosity::Summary,
376                "\t\tBackward propagated interval to k-1: {a_min_next} <= a[k] <= {a_max_next}."
377            );
378        }
379    }
380
381    if BIDIRECTION {
382        if verboser.is_enabled(Verbosity::Debug) {
383            crate::verbosity_log!(crate::diag::Verbosity::Summary, "Forward pass started.");
384        }
385
386        *a_max.first_mut().unwrap() = problem.a_boundary.0;
387        *a_min.first_mut().unwrap() = problem.a_boundary.0;
388        let mut a_max_prev = problem.a_boundary.0;
389        let mut a_min_prev = problem.a_boundary.0;
390        for (k, (a_max_k, a_min_k)) in a_max.iter_mut().zip(a_min.iter_mut()).enumerate().skip(1) {
391            let idx_s = idx_s_start + k;
392            if verboser.is_enabled(Verbosity::Trace) {
393                crate::verbosity_log!(
394                    crate::diag::Verbosity::Summary,
395                    "\tForward pass at k = {k} (idx_s = {idx_s}): prev interval {} <= a[k-1] <= {}, backward interval {} <= a[k] <= {}.",
396                    a_min_prev,
397                    a_max_prev,
398                    *a_min_k,
399                    *a_max_k
400                );
401            }
402
403            a_b.clear();
404            a_b.push((1.0, 0.0, a_max_prev));
405            a_b.push((-1.0, 0.0, -a_min_prev));
406            problem
407                .constraints
408                .fill_acc_topp2::<false>(&mut a_b, idx_s_start + k - 1);
409            // a_b.0 * a[k-1] + a_b.1 * a[k] <= a_b.2
410
411            let a_prev_mid = 0.5 * (a_max_prev + a_min_prev);
412            let (a_max_curr, a_min_curr) = match approx_order(
413                a_max_prev,
414                a_min_prev,
415                options.a_cmp_abs_tol,
416                options.a_cmp_rel_tol,
417            ) {
418                ApproxOrdering::Equal => {
419                    lp_1d::<true>(
420                        a_b.iter().skip(2).map(|&coeffs| {
421                            // coeffs.0 * a_prev  + coeffs.1* a_curr <= coeffs.2
422                            // coeffs.1 * a_curr <= coeffs.2 - coeffs.0 * a_prev
423                            (coeffs.1, coeffs.2 - coeffs.0 * a_prev_mid)
424                        }),
425                        &LpToleranceOptions::with_feas_tol(options.lp_feas_tol),
426                    )
427                }
428                ApproxOrdering::Greater => {
429                    // Check whether the forward pass of `amax_curr` can be skipped
430                    let (a_test_max, a_test_min) = lp_1d::<true>(
431                        a_b.iter().skip(2).map(|&coeffs| {
432                            // coeffs.0 * a_prev  + coeffs.1* a_curr <= coeffs.2
433                            // coeffs.0 * a_prev <= coeffs.2 - coeffs.1 * a_curr
434                            (coeffs.0, coeffs.2 - coeffs.1 * *a_max_k)
435                        }),
436                        &LpToleranceOptions::with_feas_tol(options.lp_feas_tol),
437                    );
438                    let flag_need_max = a_test_max.is_nan()
439                        || a_test_min.is_nan()
440                        || a_test_max < a_min_prev
441                        || a_test_max > a_max_prev;
442                    // Check whether the forward pass of `amin_curr` can be skipped
443                    let (a_test_max, a_test_min) = lp_1d::<true>(
444                        a_b.iter().skip(2).map(|&coeffs| {
445                            // coeffs.0 * a_prev  + coeffs.1* a_curr <= coeffs.2
446                            // coeffs.0 * a_prev <= coeffs.2 - coeffs.1 * a_curr
447                            (coeffs.0, coeffs.2 - coeffs.1 * *a_min_k)
448                        }),
449                        &LpToleranceOptions::with_feas_tol(options.lp_feas_tol),
450                    );
451                    let flag_need_min = a_test_max.is_nan()
452                        || a_test_min.is_nan()
453                        || a_test_max < a_min_prev
454                        || a_test_max > a_max_prev;
455
456                    if verboser.is_enabled(Verbosity::Trace) {
457                        crate::verbosity_log!(
458                            crate::diag::Verbosity::Summary,
459                            "\t\tForward skip checks at idx_s = {idx_s}: need_max = {flag_need_max}, need_min = {flag_need_min}."
460                        );
461                    }
462
463                    // forward and backward is the same
464                    match (flag_need_max, flag_need_min) {
465                        (true, true) => backward_bound_a_next::<true, true>(
466                            &mut a_b,
467                            a_prev_mid,
468                            options.lp_feas_tol,
469                        ),
470                        (true, false) => (
471                            backward_bound_a_next::<true, false>(
472                                &mut a_b,
473                                a_prev_mid,
474                                options.lp_feas_tol,
475                            )
476                            .0,
477                            *a_min_k,
478                        ),
479                        (false, true) => (
480                            *a_max_k,
481                            backward_bound_a_next::<false, true>(
482                                &mut a_b,
483                                a_prev_mid,
484                                options.lp_feas_tol,
485                            )
486                            .1,
487                        ),
488                        (false, false) => (*a_max_k, *a_min_k),
489                    }
490                }
491                ApproxOrdering::Less => {
492                    let err = CoppError::Infeasible(
493                        "reach_set2".into(),
494                        format!(
495                            "The reachable set is empty at index {idx_s} during the forward pass where a_max_prev = {a_max_prev}, a_min_prev = {a_min_prev}"
496                        ),
497                    );
498                    if verboser.is_enabled(Verbosity::Debug) {
499                        crate::verbosity_log!(crate::diag::Verbosity::Summary, "{err:?}");
500                    } else if verboser.is_enabled(Verbosity::Summary) {
501                        crate::verbosity_log!(
502                            crate::diag::Verbosity::Debug,
503                            "reach_set2: the forward pass failed at index {idx_s} due to infeasibility."
504                        );
505                    }
506                    return Err(err);
507                }
508            };
509
510            if verboser.is_enabled(Verbosity::Trace) {
511                crate::verbosity_log!(
512                    crate::diag::Verbosity::Summary,
513                    "\t\tForward LP result at idx_s = {idx_s}: {a_min_curr} <= a[k] <= {a_max_curr}."
514                );
515            }
516
517            if a_max_curr.is_nan() || a_min_curr.is_nan() {
518                let err = CoppError::Infeasible(
519                    "reach_set2".into(),
520                    format!(
521                        "The reachable set is empty at index {idx_s} during the forward pass where a_max = {a_max_curr}, a_min = {a_min_curr}"
522                    ),
523                );
524                if verboser.is_enabled(Verbosity::Debug) {
525                    crate::verbosity_log!(crate::diag::Verbosity::Summary, "{err:?}");
526                } else if verboser.is_enabled(Verbosity::Summary) {
527                    crate::verbosity_log!(
528                        crate::diag::Verbosity::Debug,
529                        "reach_set2: the forward pass failed at index {idx_s} due to infeasibility."
530                    );
531                }
532                return Err(err);
533            }
534
535            a_max_prev = a_max_curr.min(*a_max_k);
536            a_min_prev = a_min_curr.max(*a_min_k);
537            if verboser.is_enabled(Verbosity::Trace) {
538                crate::verbosity_log!(
539                    crate::diag::Verbosity::Summary,
540                    "\t\tAfter intersecting with backward interval at idx_s = {idx_s}: {a_min_prev} <= a[k] <= {a_max_prev}."
541                );
542            }
543            match approx_order(
544                a_max_prev,
545                a_min_prev,
546                options.a_cmp_abs_tol,
547                options.a_cmp_rel_tol,
548            ) {
549                ApproxOrdering::Less => {
550                    let err = CoppError::Infeasible(
551                        "reach_set2".into(),
552                        format!(
553                            "The reachable set is empty at index {idx_s} during the forward pass where a_max = {a_max_prev}, a_min = {a_min_prev}"
554                        ),
555                    );
556                    if verboser.is_enabled(Verbosity::Debug) {
557                        crate::verbosity_log!(crate::diag::Verbosity::Summary, "{err:?}");
558                    } else if verboser.is_enabled(Verbosity::Summary) {
559                        crate::verbosity_log!(
560                            crate::diag::Verbosity::Debug,
561                            "reach_set2: the forward pass failed at index {idx_s} due to infeasibility."
562                        );
563                    }
564                    return Err(err);
565                }
566                ApproxOrdering::Equal => {
567                    if verboser.is_enabled(Verbosity::Debug) && k < n - 1 {
568                        crate::verbosity_log!(
569                            crate::diag::Verbosity::Summary,
570                            "The forward reachable set at idx_s = {idx_s} is degenerate since a_max_prev and a_min_prev are approximately equal at {a_prev_mid}."
571                        );
572                    }
573                    a_max_prev = 0.5 * (a_max_prev + a_min_prev);
574                    a_min_prev = a_max_prev;
575                }
576                ApproxOrdering::Greater => {}
577            }
578            if verboser.is_enabled(Verbosity::Trace) {
579                crate::verbosity_log!(
580                    crate::diag::Verbosity::Summary,
581                    "\t\tForward propagated interval to next step: {a_min_prev} <= a[k] <= {a_max_prev}."
582                );
583            }
584            *a_max_k = a_max_prev;
585            *a_min_k = a_min_prev;
586        }
587    }
588
589    if verboser.is_enabled(Verbosity::Summary) {
590        crate::verbosity_log!(
591            crate::diag::Verbosity::Summary,
592            "reach_set2: {}backward total elapsed time = {}.",
593            if BIDIRECTION { "forward + " } else { "" },
594            format_duration_human(verboser.elapsed())
595        );
596    }
597
598    Ok(ReachSet2 { a_max, a_min })
599}
600
601/// Backward propagation to compute feasible `a[k]` bounds at the current step given the next step's `a[k+1]=a_next` bounds.
602/// a_b.0 * a[k+1] + a_b.1 * a[k] <= a_b.2
603/// Returns (a_max_curr, a_min_curr)
604fn backward_bound_a_next<const MAX: bool, const MIN: bool>(
605    a_b: &mut [(f64, f64, f64)],
606    a_next_mid: f64,
607    lp_fea_tol: f64,
608) -> (f64, f64) {
609    let warm_start = Lp2dWarmStart {
610        x0: (a_next_mid, LP_BOUND),
611        skip: 2,
612    };
613
614    normalize_lp2d(a_b);
615    let a_curr_max = if MAX {
616        let (_, a_curr_max) = lp_2d_incre_max_y::<_, false>(
617            a_b,
618            &warm_start,
619            &LpToleranceOptions::with_feas_tol(lp_fea_tol),
620        );
621        a_curr_max
622    } else {
623        f64::INFINITY
624    };
625
626    let a_curr_min = if MIN {
627        // Better than transform the sign in the lp_2d_incre function.
628        a_b.iter_mut().for_each(|(_, b, _)| {
629            *b = -*b;
630        });
631        let (_, a_curr_min_neg) = lp_2d_incre_max_y::<_, false>(
632            a_b,
633            &warm_start,
634            &LpToleranceOptions::with_feas_tol(lp_fea_tol),
635        );
636        -(a_curr_min_neg.min(0.0))
637    } else {
638        0.0
639    };
640
641    (a_curr_max, a_curr_min)
642}
643
644/// Builder for [`ReachSet2Options`](crate::solver::reach_set2::ReachSet2Options).
645pub struct ReachSet2OptionsBuilder {
646    /// Feasibility tolerance used by LP subproblems in reachable-set computation.
647    pub lp_feas_tol: f64,
648    /// Absolute tolerance for comparing interval bounds `a_max` and `a_min`.
649    pub a_cmp_abs_tol: f64,
650    /// Relative tolerance for comparing interval bounds `a_max` and `a_min`.
651    pub a_cmp_rel_tol: f64,
652    /// Verbosity level for diagnostics during reachability analysis.
653    pub verbosity: Verbosity,
654}
655
656impl Default for ReachSet2OptionsBuilder {
657    #[inline]
658    fn default() -> Self {
659        Self {
660            lp_feas_tol: 1e-8,
661            a_cmp_abs_tol: 1e-8,
662            a_cmp_rel_tol: 1e-8,
663            verbosity: Verbosity::default(),
664        }
665    }
666}
667
668impl ReachSet2OptionsBuilder {
669    /// Create a new [`ReachSet2OptionsBuilder`](crate::solver::reach_set2::ReachSet2OptionsBuilder) with default values.
670    pub fn new() -> Self {
671        Default::default()
672    }
673
674    /// Set the tolerance for checking the feasibility of the linear program.
675    /// The default value is 1E-8.
676    pub fn lp_feas_tol(mut self, tol: f64) -> Self {
677        self.lp_feas_tol = tol;
678        self
679    }
680
681    /// Set the absolute tolerance for comparing `a_max` and `a_min` to determine whether the reachable set is empty (`a_max < a_min`) or degenerated (`a_max == a_min`).
682    /// Let `tol = max(a_cmp_abs_tol, a_cmp_rel_tol * max(|a_max|, |a_min|))`.
683    /// + If `a_max < a_min - tol`, then the reachable set is empty.
684    /// + If `a_max > a_min + tol`, then the reachable set is non-degenerated.
685    /// + Otherwise, the reachable set is degenerated into a single point.
686    ///
687    /// The default value is 1E-8.
688    #[inline]
689    pub fn a_cmp_abs_tol(mut self, tol: f64) -> Self {
690        self.a_cmp_abs_tol = tol;
691        self
692    }
693
694    /// Set the relative tolerance for comparing `a_max` and `a_min`. More details refer to `a_cmp_abs_tol`.
695    /// The default value is 1E-8.
696    #[inline]
697    pub fn a_cmp_rel_tol(mut self, tol: f64) -> Self {
698        self.a_cmp_rel_tol = tol;
699        self
700    }
701
702    /// Set the verbosity level for logging. More details refer to [`Verbosity`](crate::diag::Verbosity).
703    /// The default value is [`Verbosity::Silent`](crate::diag::Verbosity::Silent).
704    #[inline]
705    pub fn verbosity(mut self, verbosity: Verbosity) -> Self {
706        self.verbosity = verbosity;
707        self
708    }
709
710    /// Build the [`ReachSet2Options`](crate::solver::reach_set2::ReachSet2Options) from the builder where the validity of the options is checked.
711    #[inline]
712    pub fn build(self) -> Result<ReachSet2Options, CoppError> {
713        self.validate()?;
714        Ok(ReachSet2Options {
715            lp_feas_tol: self.lp_feas_tol,
716            a_cmp_abs_tol: self.a_cmp_abs_tol,
717            a_cmp_rel_tol: self.a_cmp_rel_tol,
718            verbosity: self.verbosity,
719        })
720    }
721
722    /// This function checks the validity of the options and returns an error if any option is invalid.
723    #[inline]
724    pub fn validate(&self) -> Result<(), CoppError> {
725        check_strictly_positive("ReachSet2OptionsBuilder", "lp_feas_tol", self.lp_feas_tol)?;
726        check_abs_rel_tol(
727            "ReachSet2OptionsBuilder",
728            "a_cmp_abs_tol",
729            self.a_cmp_abs_tol,
730            "a_cmp_rel_tol",
731            self.a_cmp_rel_tol,
732        )?;
733        Ok(())
734    }
735}
736
737/// The options for [`reach_set2`](crate::solver::reach_set2).
738pub struct ReachSet2Options {
739    pub(crate) lp_feas_tol: f64,
740    pub(crate) a_cmp_abs_tol: f64,
741    pub(crate) a_cmp_rel_tol: f64,
742    pub(crate) verbosity: Verbosity,
743}
744
745impl ReachSet2Options {
746    #[inline]
747    /// Return the LP feasibility tolerance used by reachability subproblems.
748    pub fn lp_feas_tol(&self) -> f64 {
749        self.lp_feas_tol
750    }
751    #[inline]
752    /// Return the absolute tolerance used for comparing `a` bounds.
753    pub fn a_cmp_abs_tol(&self) -> f64 {
754        self.a_cmp_abs_tol
755    }
756    #[inline]
757    /// Return the relative tolerance used for comparing `a` bounds.
758    pub fn a_cmp_rel_tol(&self) -> f64 {
759        self.a_cmp_rel_tol
760    }
761    #[inline]
762    /// Return the verbosity level used for reach-set diagnostics.
763    pub fn verbosity(&self) -> Verbosity {
764        self.verbosity
765    }
766}
767
768#[cfg(test)]
769mod tests {
770    use super::*;
771    use crate::copp::copp2::stable::basic::Topp2ProblemBuilder;
772    use crate::copp::copp2::stable::reach_set2::reach_set2_backward;
773    use crate::copp::copp2::stable::topp2_ra::topp2_ra;
774    use crate::path::{add_symmetric_axial_limits_for_test, lissajous_path_for_test};
775    use crate::robot::robot_core::Robot;
776
777    #[test]
778    fn test_reach_set2() -> Result<(), CoppError> {
779        let dim = 7;
780        let n: usize = 1000;
781
782        let options = ReachSet2OptionsBuilder::new()
783            .lp_feas_tol(1E-9)
784            .a_cmp_abs_tol(1E-9)
785            .a_cmp_rel_tol(1E-9)
786            .verbosity(Verbosity::Summary)
787            .build()?;
788
789        let mut robot = Robot::with_capacity(dim, n);
790        let mut rng = rand::rng();
791
792        let (s, path, _, _) = lissajous_path_for_test(dim, n, &mut rng).map_err(|e| {
793            CoppError::InvalidInput(
794                "test_reach_set2_with_path_helper".into(),
795                format!("failed to generate test path: {e}"),
796            )
797        })?;
798
799        robot
800            .with_s(&s.as_view())?
801            .with_q_from_path_3rd(&path, 0, n)?;
802
803        add_symmetric_axial_limits_for_test(&mut robot, 1.0, 1.0, Some(5.0)).map_err(|e| {
804            CoppError::InvalidInput(
805                "test_reach_set2_with_path_helper".into(),
806                format!("failed to add symmetric axial limits: {e}"),
807            )
808        })?;
809
810        let topp2_problem = Topp2ProblemBuilder::new(&robot, (0, n - 1), (0.0, 0.0)).build()?;
811
812        let reach_set_back = reach_set2_backward(&topp2_problem, &options)?;
813        let reach_set_for = reach_set2_bidirectional(&topp2_problem, &options)?;
814        let a_ra = topp2_ra(&topp2_problem, &options)?;
815
816        crate::verbosity_log!(
817            crate::diag::Verbosity::Summary,
818            "a_max_back.len() = {};",
819            reach_set_back.a_max.len()
820        );
821        crate::verbosity_log!(
822            crate::diag::Verbosity::Summary,
823            "a_max_for.len() = {};",
824            reach_set_for.a_max.len()
825        );
826        crate::verbosity_log!(
827            crate::diag::Verbosity::Summary,
828            "a_ra.len() = {};",
829            a_ra.len()
830        );
831
832        Ok(())
833    }
834}