1use 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 EPS_ZERO, LP_BOUND, Lp2dWarmStart, LpToleranceOptions, lp_1d, lp_2d_incre_max_y, normalize_lp2d,
28};
29use core::f64;
30
31pub struct ReachSet2 {
34 pub a_max: Vec<f64>,
36 pub a_min: Vec<f64>,
38}
39
40#[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#[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
97fn 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 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 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 a_b.push((0.0, 1.0, problem.constraints.amax_unchecked(idx_s)));
215 problem.constraints.fill_acc_topp2::<true>(&mut a_b, idx_s);
216 normalize_lp2d(&mut a_b);
218
219 let a_next_mid = 0.5 * (a_max_next + a_min_next);
220 let (a_max_curr, a_min_curr) = match approx_order(
221 a_max_next,
222 a_min_next,
223 options.a_cmp_abs_tol,
224 options.a_cmp_rel_tol,
225 ) {
226 ApproxOrdering::Equal => {
227 if verboser.is_enabled(Verbosity::Trace) {
228 crate::verbosity_log!(
229 crate::diag::Verbosity::Summary,
230 "\t\ta[k+1] = {a_next_mid}"
231 );
232 }
233 lp_1d::<true>(
234 a_b.iter().skip(2).map(|&coeffs| {
235 (coeffs.1, coeffs.2 - coeffs.0 * a_next_mid)
238 }),
239 &LpToleranceOptions::with_feas_tol(options.lp_feas_tol),
240 )
241 }
242 ApproxOrdering::Greater => {
243 if verboser.is_enabled(Verbosity::Trace) {
244 crate::verbosity_log!(
245 crate::diag::Verbosity::Summary,
246 "\t\t{a_min_next} <= a[k+1] <= {a_max_next}"
247 );
248 }
249 let (a_test_max, a_test_min) = lp_1d::<true>(
251 a_b.iter().skip(2).map(|&coeffs| {
252 (coeffs.0, coeffs.2 - coeffs.1 * *a_min_k)
255 }),
256 &LpToleranceOptions::with_feas_tol(options.lp_feas_tol),
257 );
258 let flag_need_min = a_test_max.is_nan()
259 || a_test_min.is_nan()
260 || a_test_max < a_min_next
261 || a_test_max > a_max_next;
262 if verboser.is_enabled(Verbosity::Trace) {
263 crate::verbosity_log!(
264 crate::diag::Verbosity::Summary,
265 "\t\tBackward skip checks at idx_s = {idx_s}: need_max = true, need_min = {flag_need_min}."
266 );
267 }
268 if flag_need_min {
269 backward_bound_a_next::<true, true>(&mut a_b, a_next_mid, options.lp_feas_tol)
270 } else {
271 (
272 backward_bound_a_next::<true, false>(
273 &mut a_b,
274 a_next_mid,
275 options.lp_feas_tol,
276 )
277 .0,
278 *a_min_k,
279 )
280 }
281 }
282 ApproxOrdering::Less => {
283 let err = CoppError::Infeasible(
284 "reach_set2".into(),
285 format!(
286 "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}"
287 ),
288 );
289 if verboser.is_enabled(Verbosity::Debug) {
290 crate::verbosity_log!(crate::diag::Verbosity::Summary, "{err:?}");
291 } else if verboser.is_enabled(Verbosity::Summary) {
292 crate::verbosity_log!(
293 crate::diag::Verbosity::Debug,
294 "reach_set2: the backward pass failed at index {idx_s} due to infeasibility."
295 );
296 }
297 return Err(err);
298 }
299 };
300
301 if a_max_curr.is_nan() || a_min_curr.is_nan() {
302 let err = CoppError::Infeasible(
303 "reach_set2".into(),
304 format!(
305 "The reachable set is empty at index {idx_s} during the backward pass where a_max = {a_max_curr}, a_min = {a_min_curr}"
306 ),
307 );
308 if verboser.is_enabled(Verbosity::Debug) {
309 crate::verbosity_log!(crate::diag::Verbosity::Summary, "{err:?}");
310 } else if verboser.is_enabled(Verbosity::Summary) {
311 crate::verbosity_log!(
312 crate::diag::Verbosity::Debug,
313 "reach_set2: the backward pass failed at index {idx_s} due to infeasibility."
314 );
315 }
316 return Err(err);
317 }
318 if verboser.is_enabled(Verbosity::Trace) {
319 crate::verbosity_log!(
320 crate::diag::Verbosity::Summary,
321 "\t\tBackward LP result at k = {k}: {a_min_curr} <= a[k] <= {a_max_curr}."
322 );
323 }
324
325 *a_max_k = a_max_curr.min(problem.constraints.amax_unchecked(idx_s));
326 *a_min_k = a_min_curr.max(0.0);
327
328 if verboser.is_enabled(Verbosity::Trace) {
329 crate::verbosity_log!(
330 crate::diag::Verbosity::Summary,
331 "\t\tAfter clipping with path constraints at k = {k}: {} <= a[k] <= {}.",
332 *a_min_k,
333 *a_max_k
334 );
335 }
336
337 match approx_order(
340 *a_max_k,
341 *a_min_k,
342 options
343 .a_cmp_abs_tol
344 .max(options.a_cmp_rel_tol * a_max_next.abs()),
345 options.a_cmp_rel_tol,
346 ) {
347 ApproxOrdering::Less => {
348 let err = CoppError::Infeasible(
349 "reach_set2".into(),
350 format!(
351 "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}"
352 ),
353 );
354 if verboser.is_enabled(Verbosity::Debug) {
355 crate::verbosity_log!(crate::diag::Verbosity::Summary, "{err:?}");
356 } else if verboser.is_enabled(Verbosity::Summary) {
357 crate::verbosity_log!(
358 crate::diag::Verbosity::Debug,
359 "reach_set2: the backward pass failed at k = {k} (idx_s = {idx_s}) due to infeasibility."
360 );
361 }
362 return Err(err);
363 }
364 ApproxOrdering::Equal => {
365 if verboser.is_enabled(Verbosity::Debug) && k < n - 1 {
366 crate::verbosity_log!(
367 crate::diag::Verbosity::Summary,
368 "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."
369 );
370 }
371 *a_max_k = 0.5 * (*a_max_k + *a_min_k);
372 *a_min_k = *a_max_k;
373 }
374 ApproxOrdering::Greater => {}
375 }
376 a_max_next = *a_max_k;
377 a_min_next = *a_min_k;
378
379 if verboser.is_enabled(Verbosity::Trace) {
380 crate::verbosity_log!(
381 crate::diag::Verbosity::Summary,
382 "\t\tBackward propagated interval to k-1: {a_min_next} <= a[k] <= {a_max_next}."
383 );
384 }
385 }
386
387 if BIDIRECTION {
388 if verboser.is_enabled(Verbosity::Debug) {
389 crate::verbosity_log!(crate::diag::Verbosity::Summary, "Forward pass started.");
390 }
391
392 *a_max.first_mut().unwrap() = problem.a_boundary.0;
393 *a_min.first_mut().unwrap() = problem.a_boundary.0;
394 let mut a_max_prev = problem.a_boundary.0;
395 let mut a_min_prev = problem.a_boundary.0;
396 for (k, (a_max_k, a_min_k)) in a_max.iter_mut().zip(a_min.iter_mut()).enumerate().skip(1) {
397 let idx_s = idx_s_start + k;
398 if verboser.is_enabled(Verbosity::Trace) {
399 crate::verbosity_log!(
400 crate::diag::Verbosity::Summary,
401 "\tForward pass at k = {k} (idx_s = {idx_s}): prev interval {} <= a[k-1] <= {}, backward interval {} <= a[k] <= {}.",
402 a_min_prev,
403 a_max_prev,
404 *a_min_k,
405 *a_max_k
406 );
407 }
408
409 a_b.clear();
410 a_b.push((1.0, 0.0, a_max_prev));
411 a_b.push((-1.0, 0.0, -a_min_prev));
412 a_b.push((0.0, 1.0, problem.constraints.amax_unchecked(idx_s)));
413 problem
414 .constraints
415 .fill_acc_topp2::<false>(&mut a_b, idx_s_start + k - 1);
416 normalize_lp2d(&mut a_b);
418
419 let a_prev_mid = 0.5 * (a_max_prev + a_min_prev);
420 let (a_max_curr, a_min_curr) = match approx_order(
421 a_max_prev,
422 a_min_prev,
423 options.a_cmp_abs_tol,
424 options.a_cmp_rel_tol,
425 ) {
426 ApproxOrdering::Equal => {
427 lp_1d::<true>(
428 a_b.iter().skip(2).map(|&coeffs| {
429 (coeffs.1, coeffs.2 - coeffs.0 * a_prev_mid)
432 }),
433 &LpToleranceOptions::with_feas_tol(options.lp_feas_tol),
434 )
435 }
436 ApproxOrdering::Greater => {
437 let (a_test_max, a_test_min) = lp_1d::<true>(
439 a_b.iter().skip(2).map(|&coeffs| {
440 (coeffs.0, coeffs.2 - coeffs.1 * *a_max_k)
443 }),
444 &LpToleranceOptions::with_feas_tol(options.lp_feas_tol),
445 );
446 let flag_need_max = a_test_max.is_nan()
447 || a_test_min.is_nan()
448 || a_test_max < a_min_prev
449 || a_test_max > a_max_prev;
450 let (a_test_max, a_test_min) = lp_1d::<true>(
452 a_b.iter().skip(2).map(|&coeffs| {
453 (coeffs.0, coeffs.2 - coeffs.1 * *a_min_k)
456 }),
457 &LpToleranceOptions::with_feas_tol(options.lp_feas_tol),
458 );
459 let flag_need_min = a_test_max.is_nan()
460 || a_test_min.is_nan()
461 || a_test_max < a_min_prev
462 || a_test_max > a_max_prev;
463
464 if verboser.is_enabled(Verbosity::Trace) {
465 crate::verbosity_log!(
466 crate::diag::Verbosity::Summary,
467 "\t\tForward skip checks at idx_s = {idx_s}: need_max = {flag_need_max}, need_min = {flag_need_min}."
468 );
469 }
470
471 match (flag_need_max, flag_need_min) {
473 (true, true) => backward_bound_a_next::<true, true>(
474 &mut a_b,
475 a_prev_mid,
476 options.lp_feas_tol,
477 ),
478 (true, false) => (
479 backward_bound_a_next::<true, false>(
480 &mut a_b,
481 a_prev_mid,
482 options.lp_feas_tol,
483 )
484 .0,
485 *a_min_k,
486 ),
487 (false, true) => (
488 *a_max_k,
489 backward_bound_a_next::<false, true>(
490 &mut a_b,
491 a_prev_mid,
492 options.lp_feas_tol,
493 )
494 .1,
495 ),
496 (false, false) => (*a_max_k, *a_min_k),
497 }
498 }
499 ApproxOrdering::Less => {
500 let err = CoppError::Infeasible(
501 "reach_set2".into(),
502 format!(
503 "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}"
504 ),
505 );
506 if verboser.is_enabled(Verbosity::Debug) {
507 crate::verbosity_log!(crate::diag::Verbosity::Summary, "{err:?}");
508 } else if verboser.is_enabled(Verbosity::Summary) {
509 crate::verbosity_log!(
510 crate::diag::Verbosity::Debug,
511 "reach_set2: the forward pass failed at index {idx_s} due to infeasibility."
512 );
513 }
514 return Err(err);
515 }
516 };
517
518 if verboser.is_enabled(Verbosity::Trace) {
519 crate::verbosity_log!(
520 crate::diag::Verbosity::Summary,
521 "\t\tForward LP result at idx_s = {idx_s}: {a_min_curr} <= a[k] <= {a_max_curr}."
522 );
523 }
524
525 if a_max_curr.is_nan() || a_min_curr.is_nan() {
526 let err = CoppError::Infeasible(
527 "reach_set2".into(),
528 format!(
529 "The reachable set is empty at index {idx_s} during the forward pass where a_max = {a_max_curr}, a_min = {a_min_curr}"
530 ),
531 );
532 if verboser.is_enabled(Verbosity::Debug) {
533 crate::verbosity_log!(crate::diag::Verbosity::Summary, "{err:?}");
534 } else if verboser.is_enabled(Verbosity::Summary) {
535 crate::verbosity_log!(
536 crate::diag::Verbosity::Debug,
537 "reach_set2: the forward pass failed at index {idx_s} due to infeasibility."
538 );
539 }
540 return Err(err);
541 }
542
543 let a_prev_scale = a_max_prev.abs();
544 a_max_prev = a_max_curr.min(*a_max_k);
545 a_min_prev = a_min_curr.max(*a_min_k);
546 if verboser.is_enabled(Verbosity::Trace) {
547 crate::verbosity_log!(
548 crate::diag::Verbosity::Summary,
549 "\t\tAfter intersecting with backward interval at idx_s = {idx_s}: {a_min_prev} <= a[k] <= {a_max_prev}."
550 );
551 }
552 match approx_order(
553 a_max_prev,
554 a_min_prev,
555 options
556 .a_cmp_abs_tol
557 .max(options.a_cmp_rel_tol * a_prev_scale),
558 options.a_cmp_rel_tol,
559 ) {
560 ApproxOrdering::Less => {
561 let err = CoppError::Infeasible(
562 "reach_set2".into(),
563 format!(
564 "The reachable set is empty at index {idx_s} during the forward pass where a_max = {a_max_prev}, a_min = {a_min_prev}"
565 ),
566 );
567 if verboser.is_enabled(Verbosity::Debug) {
568 crate::verbosity_log!(crate::diag::Verbosity::Summary, "{err:?}");
569 } else if verboser.is_enabled(Verbosity::Summary) {
570 crate::verbosity_log!(
571 crate::diag::Verbosity::Debug,
572 "reach_set2: the forward pass failed at index {idx_s} due to infeasibility."
573 );
574 }
575 return Err(err);
576 }
577 ApproxOrdering::Equal => {
578 if verboser.is_enabled(Verbosity::Debug) && k < n - 1 {
579 crate::verbosity_log!(
580 crate::diag::Verbosity::Summary,
581 "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}."
582 );
583 }
584 a_max_prev = 0.5 * (a_max_prev + a_min_prev);
585 a_min_prev = a_max_prev;
586 }
587 ApproxOrdering::Greater => {}
588 }
589 if verboser.is_enabled(Verbosity::Trace) {
590 crate::verbosity_log!(
591 crate::diag::Verbosity::Summary,
592 "\t\tForward propagated interval to next step: {a_min_prev} <= a[k] <= {a_max_prev}."
593 );
594 }
595 *a_max_k = a_max_prev;
596 *a_min_k = a_min_prev;
597 }
598 }
599
600 if verboser.is_enabled(Verbosity::Summary) {
601 crate::verbosity_log!(
602 crate::diag::Verbosity::Summary,
603 "reach_set2: {}backward total elapsed time = {}.",
604 if BIDIRECTION { "forward + " } else { "" },
605 format_duration_human(verboser.elapsed())
606 );
607 }
608
609 Ok(ReachSet2 { a_max, a_min })
610}
611
612fn backward_bound_a_next<const MAX: bool, const MIN: bool>(
618 a_b: &mut [(f64, f64, f64)],
619 a_next_mid: f64,
620 lp_fea_tol: f64,
621) -> (f64, f64) {
622 let a_next_abs = a_b[0].2.abs().max(a_b[1].2.abs());
627 let a_curr_upper = a_b
628 .iter()
629 .skip(2)
630 .filter(|&&(_, b, _)| b > EPS_ZERO)
631 .map(|&(a, b, c)| (c + a.abs() * a_next_abs) / b)
632 .fold(f64::INFINITY, f64::min);
633 let warm_start = Lp2dWarmStart {
634 x0: (
635 a_next_mid,
636 if a_curr_upper.is_finite() {
637 a_curr_upper
638 } else {
639 LP_BOUND
640 },
641 ),
642 skip: 3,
643 };
644
645 let a_curr_max = if MAX {
646 let (_, a_curr_max) = lp_2d_incre_max_y::<_, false>(
647 a_b,
648 &warm_start,
649 &LpToleranceOptions::with_feas_tol(lp_fea_tol),
650 );
651 a_curr_max
652 } else {
653 f64::INFINITY
654 };
655
656 let a_curr_min = if MIN {
657 a_b.iter_mut().for_each(|(_, b, _)| {
659 *b = -*b;
660 });
661 let (_, a_curr_min_neg) = lp_2d_incre_max_y::<_, false>(
662 a_b,
663 &warm_start,
664 &LpToleranceOptions::with_feas_tol(lp_fea_tol),
665 );
666 -(a_curr_min_neg.min(0.0))
667 } else {
668 0.0
669 };
670
671 (a_curr_max, a_curr_min)
672}
673
674pub struct ReachSet2OptionsBuilder {
676 pub lp_feas_tol: f64,
678 pub a_cmp_abs_tol: f64,
680 pub a_cmp_rel_tol: f64,
682 pub verbosity: Verbosity,
684}
685
686impl Default for ReachSet2OptionsBuilder {
687 #[inline]
688 fn default() -> Self {
689 Self {
690 lp_feas_tol: 1e-8,
691 a_cmp_abs_tol: 1e-8,
692 a_cmp_rel_tol: 1e-8,
693 verbosity: Verbosity::default(),
694 }
695 }
696}
697
698impl ReachSet2OptionsBuilder {
699 pub fn new() -> Self {
701 Default::default()
702 }
703
704 pub fn lp_feas_tol(mut self, tol: f64) -> Self {
707 self.lp_feas_tol = tol;
708 self
709 }
710
711 #[inline]
720 pub fn a_cmp_abs_tol(mut self, tol: f64) -> Self {
721 self.a_cmp_abs_tol = tol;
722 self
723 }
724
725 #[inline]
728 pub fn a_cmp_rel_tol(mut self, tol: f64) -> Self {
729 self.a_cmp_rel_tol = tol;
730 self
731 }
732
733 #[inline]
736 pub fn verbosity(mut self, verbosity: Verbosity) -> Self {
737 self.verbosity = verbosity;
738 self
739 }
740
741 #[inline]
743 pub fn build(self) -> Result<ReachSet2Options, CoppError> {
744 self.validate()?;
745 Ok(ReachSet2Options {
746 lp_feas_tol: self.lp_feas_tol,
747 a_cmp_abs_tol: self.a_cmp_abs_tol,
748 a_cmp_rel_tol: self.a_cmp_rel_tol,
749 verbosity: self.verbosity,
750 })
751 }
752
753 #[inline]
755 pub fn validate(&self) -> Result<(), CoppError> {
756 check_strictly_positive("ReachSet2OptionsBuilder", "lp_feas_tol", self.lp_feas_tol)?;
757 check_abs_rel_tol(
758 "ReachSet2OptionsBuilder",
759 "a_cmp_abs_tol",
760 self.a_cmp_abs_tol,
761 "a_cmp_rel_tol",
762 self.a_cmp_rel_tol,
763 )?;
764 Ok(())
765 }
766}
767
768pub struct ReachSet2Options {
770 pub(crate) lp_feas_tol: f64,
771 pub(crate) a_cmp_abs_tol: f64,
772 pub(crate) a_cmp_rel_tol: f64,
773 pub(crate) verbosity: Verbosity,
774}
775
776impl ReachSet2Options {
777 #[inline]
778 pub fn lp_feas_tol(&self) -> f64 {
780 self.lp_feas_tol
781 }
782 #[inline]
783 pub fn a_cmp_abs_tol(&self) -> f64 {
785 self.a_cmp_abs_tol
786 }
787 #[inline]
788 pub fn a_cmp_rel_tol(&self) -> f64 {
790 self.a_cmp_rel_tol
791 }
792 #[inline]
793 pub fn verbosity(&self) -> Verbosity {
795 self.verbosity
796 }
797}
798
799#[cfg(test)]
800mod tests {
801 use super::*;
802 use crate::copp::copp2::stable::basic::Topp2ProblemBuilder;
803 use crate::copp::copp2::stable::reach_set2::reach_set2_backward;
804 use crate::copp::copp2::stable::topp2_ra::topp2_ra;
805 use crate::path::{add_symmetric_axial_limits_for_test, lissajous_path_for_test};
806 use crate::robot::robot_core::Robot;
807
808 #[test]
809 fn test_reach_set2() -> Result<(), CoppError> {
810 let dim = 7;
811 let n: usize = 1000;
812
813 let options = ReachSet2OptionsBuilder::new()
814 .lp_feas_tol(1E-9)
815 .a_cmp_abs_tol(1E-9)
816 .a_cmp_rel_tol(1E-9)
817 .verbosity(Verbosity::Summary)
818 .build()?;
819
820 let mut robot = Robot::with_capacity(dim, n);
821 let mut rng = rand::rng();
822
823 let (s, path, _, _) = lissajous_path_for_test(dim, n, &mut rng).map_err(|e| {
824 CoppError::InvalidInput(
825 "test_reach_set2_with_path_helper".into(),
826 format!("failed to generate test path: {e}"),
827 )
828 })?;
829
830 robot
831 .with_s(&s.as_view())?
832 .with_q_from_path_3rd(&path, 0, n)?;
833
834 add_symmetric_axial_limits_for_test(&mut robot, 1.0, 1.0, Some(5.0)).map_err(|e| {
835 CoppError::InvalidInput(
836 "test_reach_set2_with_path_helper".into(),
837 format!("failed to add symmetric axial limits: {e}"),
838 )
839 })?;
840
841 let topp2_problem = Topp2ProblemBuilder::new(&robot, (0, n - 1), (0.0, 0.0)).build()?;
842
843 let reach_set_back = reach_set2_backward(&topp2_problem, &options)?;
844 let reach_set_for = reach_set2_bidirectional(&topp2_problem, &options)?;
845 let a_ra = topp2_ra(&topp2_problem, &options)?;
846
847 crate::verbosity_log!(
848 crate::diag::Verbosity::Summary,
849 "a_max_back.len() = {};",
850 reach_set_back.a_max.len()
851 );
852 crate::verbosity_log!(
853 crate::diag::Verbosity::Summary,
854 "a_max_for.len() = {};",
855 reach_set_for.a_max.len()
856 );
857 crate::verbosity_log!(
858 crate::diag::Verbosity::Summary,
859 "a_ra.len() = {};",
860 a_ra.len()
861 );
862
863 Ok(())
864 }
865}