1pub mod autodiff;
61mod path_core;
62pub mod spline;
63
64pub use autodiff::{Jet3, cos, exp, ln, powi, sin, sqrt};
65pub use path_core::{ParametricFn, Path, PathDerivatives};
66pub use spline::{Parametrization, SplineConfig};
67
68#[cfg(test)]
69use crate::diag::PathError;
70
71#[derive(Clone, Copy, Debug)]
72pub enum OutOfRangeMode {
73 Error,
75 Clamp,
77}
78
79#[cfg(test)]
84pub(crate) fn lissajous_path_for_test(
85 dim: usize,
86 s_len: usize,
87 rng: &mut impl rand::RngExt,
88) -> Result<(nalgebra::DMatrix<f64>, PathDerivatives, Vec<f64>, Vec<f64>), PathError> {
89 let omega = (0..dim)
90 .map(|_| rng.random_range(0.1..(2.0 * std::f64::consts::PI)))
91 .collect::<Vec<f64>>();
92 let phi = (0..dim)
93 .map(|_| rng.random_range(0.0..(2.0 * std::f64::consts::PI)))
94 .collect::<Vec<f64>>();
95
96 let (s, derivs) = lissajous_path_fixed_for_test(dim, s_len, omega.clone(), phi.clone())?;
97
98 Ok((s, derivs, omega, phi))
99}
100
101#[cfg(test)]
106pub(crate) fn lissajous_path_fixed_for_test(
107 dim: usize,
108 s_len: usize,
109 omega: Vec<f64>,
110 phi: Vec<f64>,
111) -> Result<(nalgebra::DMatrix<f64>, PathDerivatives), PathError> {
112 let s = nalgebra::DMatrix::<f64>::from_fn(1, s_len, |_, j| j as f64 / (s_len - 1) as f64);
113
114 let path = Path::from_parametric(
115 move |s: Jet3| {
116 (0..dim)
117 .map(|i| sin(omega[i] * s + phi[i]))
118 .collect::<Vec<Jet3>>()
119 },
120 0.0,
121 1.0,
122 )?;
123
124 let derivs = path.evaluate_up_to_3rd(s.as_slice())?;
125 Ok((s, derivs))
126}
127
128#[cfg(test)]
132pub(crate) fn add_symmetric_axial_limits_for_test<M: crate::robot::robot_core::RobotBasic>(
133 robot: &mut crate::robot::robot_core::Robot<M>,
134 vel_limit: f64,
135 acc_limit: f64,
136 jerk_limit: Option<f64>,
137) -> Result<(), crate::diag::ConstraintError> {
138 let dim = robot.dim();
139 let n = robot.constraints.len();
140 if n == 0 {
141 return Ok(());
142 }
143
144 let vel_max = vec![vel_limit; dim];
145 let vel_min = vec![-vel_limit; dim];
146 robot.with_axial_velocity((vel_max.as_slice(), n), (vel_min.as_slice(), n), 0)?;
147
148 let acc_max = vec![acc_limit; dim];
149 let acc_min = vec![-acc_limit; dim];
150 robot.with_axial_acceleration((acc_max.as_slice(), n), (acc_min.as_slice(), n), 0)?;
151
152 if let Some(jerk_limit) = jerk_limit {
153 let jerk_max = vec![jerk_limit; dim];
154 let jerk_min = vec![-jerk_limit; dim];
155 robot.with_axial_jerk((jerk_max.as_slice(), n), (jerk_min.as_slice(), n), 0)?;
156 }
157
158 Ok(())
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use crate::copp::copp2::stable::basic::{Topp2ProblemBuilder, s_to_t_topp2};
165 use crate::copp::copp2::stable::reach_set2::ReachSet2OptionsBuilder;
166 use crate::copp::copp2::stable::topp2_ra::topp2_ra;
167 use crate::copp::copp3::stable::basic::{Topp3ProblemBuilder, s_to_t_topp3, t_to_s_topp3};
168 use crate::copp::copp3::stable::topp3_lp::topp3_lp;
169 use crate::copp::{ClarabelOptionsBuilder, InterpolationMode};
170 use crate::diag::ConstraintError;
171 use crate::diag::Verbosity;
172 use crate::robot::robot_core::Robot;
173 use nalgebra::DMatrix;
174 use plotters::prelude::*;
175 use rand::RngExt;
176 use std::error::Error;
177 use std::fs::create_dir_all;
178 use std::path::Path;
179 use std::time::Instant;
180
181 const DIM: usize = 6;
182 const N: usize = 1000;
183
184 #[test]
185 #[ignore = "plotting"]
186 fn test_topp3_with_spline_path() -> Result<(), Box<dyn Error>> {
187 crate::verbosity_log!(
188 crate::diag::Verbosity::Summary,
189 "\n=== TOPP3 with Spline Path (6-DOF) ==="
190 );
191
192 let n_waypoints = 10;
193 let start = Instant::now();
194 let waypoints = make_random_waypoints(DIM, n_waypoints);
195 let sample_ms = start.elapsed().as_secs_f64() * 1e3;
196
197 let start = Instant::now();
198 let path = super::Path::from_waypoints(&waypoints, SplineConfig::default())?;
199 let build_ms = start.elapsed().as_secs_f64() * 1e3;
200
201 let s = make_s_vector(N);
202 let start = Instant::now();
203 let derivs = path.evaluate_up_to_3rd(s.as_slice())?;
204 let eval_ms = start.elapsed().as_secs_f64() * 1e3;
205
206 crate::verbosity_log!(
207 crate::diag::Verbosity::Summary,
208 "[Waypoint Sample] {sample_ms:.3} ms, [Spline Build] {build_ms:.3} ms, [Spline Eval] {eval_ms:.3} ms"
209 );
210
211 let mut robot = setup_robot_with_constraints(&s, &derivs)?;
212 let s_slice: Vec<f64> = (0..N).map(|j| s[(0, j)]).collect();
213 let (a, b, num_stat, timings) = solve_topp_pipeline(&mut robot, &s_slice)?;
214
215 crate::verbosity_log!(
216 crate::diag::Verbosity::Summary,
217 "[TOPP2-RA] {:.3} ms, t_motion = {:.3} s",
218 timings.topp2_ms,
219 timings.t_motion_2
220 );
221 crate::verbosity_log!(
222 crate::diag::Verbosity::Summary,
223 "[TOPP3-LP] {:.3} ms, t_motion = {:.3} s",
224 timings.topp3_ms,
225 timings.t_motion_3
226 );
227
228 let (t, q_t, dq_t, ddq_t, dddq_t, interp_ms) =
229 interpolate_to_time_domain(&path, &s_slice, &a, &b, num_stat, 1e-3)?;
230 crate::verbosity_log!(
231 crate::diag::Verbosity::Summary,
232 "[Interpolation] {:.3} ms, {} samples",
233 interp_ms,
234 t.len()
235 );
236
237 plot_topp3_grid(
238 "data/path_topp_plots/spline_path_topp3.png",
239 "TOPP3 Result: Spline Path (6-DOF, 50 waypoints)",
240 &t,
241 &q_t,
242 &dq_t,
243 &ddq_t,
244 &dddq_t,
245 )?;
246
247 Ok(())
248 }
249
250 fn make_s_vector(n: usize) -> DMatrix<f64> {
251 DMatrix::<f64>::from_fn(1, n, |_, j| j as f64 / (n - 1) as f64)
252 }
253
254 fn make_parametric_path_6dof() -> Result<super::Path, PathError> {
255 super::Path::from_parametric(
256 |s: Jet3| {
257 vec![
258 sin(s),
259 cos(s),
260 exp(0.3 * s) - 1.0,
261 s + 0.1 * s * s - 0.01 * s * s * s * s,
262 sin(2.0 * s) + 0.15 * cos(3.0 * s),
263 sin(s) * cos(s),
264 ]
265 },
266 0.0,
267 1.0,
268 )
269 }
270
271 fn make_random_waypoints(dim: usize, n_pts: usize) -> DMatrix<f64> {
272 let mut rng = rand::rng();
273 let mut waypoints = DMatrix::<f64>::zeros(dim, n_pts);
276 for mut row in waypoints.row_iter_mut() {
277 row[0] = rng.random_range(-1.0..1.0);
278 for j in 1..n_pts {
279 let step = rng.random_range(-0.5..0.5);
280 row[j] = row[j - 1] + step;
281 }
282 }
283 waypoints
284 }
285
286 fn setup_robot_with_constraints(
287 s: &DMatrix<f64>,
288 derivs: &PathDerivatives,
289 ) -> Result<Robot<usize>, ConstraintError> {
290 let n = s.ncols();
291 let mut robot = Robot::with_capacity(DIM, n);
292
293 robot.with_s(&s.as_view())?;
294 robot.with_q(
295 &derivs.q.as_view(),
296 &derivs.dq.as_ref().unwrap().as_view(),
297 &derivs.ddq.as_ref().unwrap().as_view(),
298 derivs.dddq.as_ref().map(|m| m.as_view()).as_ref(),
299 0,
300 )?;
301
302 let vel_max = DMatrix::<f64>::from_element(DIM, n, 1.0);
303 let vel_min = DMatrix::<f64>::from_element(DIM, n, -1.0);
304 robot.with_axial_velocity(&vel_max.as_view(), &vel_min.as_view(), 0)?;
305
306 let acc_max = DMatrix::<f64>::from_element(DIM, n, 1.0);
307 let acc_min = DMatrix::<f64>::from_element(DIM, n, -1.0);
308 robot.with_axial_acceleration(&acc_max.as_view(), &acc_min.as_view(), 0)?;
309
310 let jerk_max = DMatrix::<f64>::from_element(DIM, n, 5.0);
311 let jerk_min = DMatrix::<f64>::from_element(DIM, n, -5.0);
312 robot.with_axial_jerk(&jerk_max.as_view(), &jerk_min.as_view(), 0)?;
313
314 Ok(robot)
315 }
316
317 struct ToppTimings {
318 topp2_ms: f64,
319 topp3_ms: f64,
320 t_motion_2: f64,
321 t_motion_3: f64,
322 }
323
324 fn solve_topp_pipeline(
325 robot: &mut Robot<usize>,
326 s_slice: &[f64],
327 ) -> Result<(Vec<f64>, Vec<f64>, (usize, usize), ToppTimings), Box<dyn Error>> {
328 let n = s_slice.len();
329
330 let topp2_problem = Topp2ProblemBuilder::new(&robot, (0, n - 1), (0.0, 0.0)).build()?;
331
332 let options = ReachSet2OptionsBuilder::new()
333 .a_cmp_abs_tol(1e-9)
334 .a_cmp_rel_tol(1e-9)
335 .lp_feas_tol(1e-9)
336 .verbosity(Verbosity::Silent)
337 .build()?;
338
339 let start = Instant::now();
340 let a_ra = topp2_ra(&topp2_problem, &options)?;
341 let topp2_ms = start.elapsed().as_secs_f64() * 1e3;
342 let (t_motion_2, _) = s_to_t_topp2(s_slice, &a_ra, 0.0);
343
344 robot.constraints.amax_substitute(&a_ra, 0)?;
345
346 let topp3_problem = Topp3ProblemBuilder::new(robot, 0, &a_ra, (0.0, 0.0), (0.0, 0.0))
347 .with_num_stationary_max(2)
348 .build_with_linearization()?;
349 let options_lp = ClarabelOptionsBuilder::new()
350 .allow_almost_solved(true)
351 .build()?;
352
353 let start = Instant::now();
354 let (a_lp, b_lp, num_stationary) = topp3_lp(&topp3_problem, &options_lp)?;
355 let topp3_ms = start.elapsed().as_secs_f64() * 1e3;
356 let (t_motion_3, _) = s_to_t_topp3(s_slice, &a_lp, &b_lp, num_stationary, 0.0);
357
358 Ok((
359 a_lp,
360 b_lp,
361 num_stationary,
362 ToppTimings {
363 topp2_ms,
364 topp3_ms,
365 t_motion_2,
366 t_motion_3,
367 },
368 ))
369 }
370
371 fn interpolate_to_time_domain(
372 path: &super::Path,
373 s_slice: &[f64],
374 a: &[f64],
375 b: &[f64],
376 num_stationary: (usize, usize),
377 dt: f64,
378 ) -> Result<
379 (
380 Vec<f64>,
381 DMatrix<f64>,
382 DMatrix<f64>,
383 DMatrix<f64>,
384 DMatrix<f64>,
385 f64,
386 ),
387 Box<dyn Error>,
388 > {
389 let start = Instant::now();
390 let (t_final, t_s) = s_to_t_topp3(s_slice, a, b, num_stationary, 0.0);
391
392 let t_sample: Vec<f64> = (0..=((t_final / dt).floor() as usize))
393 .map(|i| i as f64 * dt)
394 .collect();
395
396 let s_t = t_to_s_topp3(
397 s_slice,
398 a,
399 b,
400 num_stationary,
401 &t_s,
402 InterpolationMode::NonUniformTimeGrid(&t_sample),
403 );
404
405 let n_t = t_sample.len();
407 let s_t_matrix = DMatrix::<f64>::from_row_slice(1, n_t, &s_t);
408 let q_t = path.evaluate_q(s_t_matrix.as_slice())?.q;
409
410 let mut dq_t = DMatrix::<f64>::zeros(DIM, n_t);
412 let mut ddq_t = DMatrix::<f64>::zeros(DIM, n_t);
413 let mut dddq_t = DMatrix::<f64>::zeros(DIM, n_t);
414
415 finite_diff_inplace(&q_t, &mut dq_t, dt);
417 finite_diff_inplace(&dq_t, &mut ddq_t, dt);
418 finite_diff_inplace(&ddq_t, &mut dddq_t, dt);
419
420 let interp_ms = start.elapsed().as_secs_f64() * 1e3;
421
422 Ok((t_sample, q_t, dq_t, ddq_t, dddq_t, interp_ms))
423 }
424
425 fn plot_topp3_grid(
426 filename: &str,
427 title: &str,
428 t: &[f64],
429 q_t: &DMatrix<f64>,
430 dq_t: &DMatrix<f64>,
431 ddq_t: &DMatrix<f64>,
432 dddq_t: &DMatrix<f64>,
433 ) -> Result<(), Box<dyn Error>> {
434 if let Some(parent) = Path::new(filename).parent() {
435 if !parent.as_os_str().is_empty() {
436 create_dir_all(parent)?;
437 }
438 }
439
440 let root = BitMapBackend::new(filename, (2400, 1600)).into_drawing_area();
441 root.fill(&WHITE)?;
442
443 let areas = root.split_evenly((4, DIM));
444 let mats = [q_t, dq_t, ddq_t, dddq_t];
445 let labels = ["q(t)", "dq/dt", "d²q/dt²", "d³q/dt³"];
446 let constraints_bounds = [
447 None,
448 Some((-1.0, 1.0)),
449 Some((-1.0, 1.0)),
450 Some((-5.0, 5.0)),
451 ];
452
453 for row in 0..4 {
454 for col in 0..DIM {
455 let area = &areas[row * DIM + col];
456 let series: Vec<f64> = (0..mats[row].ncols())
457 .map(|j| mats[row][(col, j)])
458 .collect();
459
460 let (mut y_min, mut y_max) = min_max(&series);
461 if let Some((lb, ub)) = constraints_bounds[row] {
462 y_min = y_min.min(lb);
463 y_max = y_max.max(ub);
464 }
465 let pad = 0.1 * (y_max - y_min).max(0.1);
466 y_min -= pad;
467 y_max += pad;
468
469 let mut chart = ChartBuilder::on(area)
470 .margin(10)
471 .caption(
472 format!("{} axis {}", labels[row], col + 1),
473 ("sans-serif", 16),
474 )
475 .x_label_area_size(30)
476 .y_label_area_size(45)
477 .build_cartesian_2d(t[0]..t[t.len() - 1], y_min..y_max)?;
478
479 chart
480 .configure_mesh()
481 .x_desc(if row == 3 { "t (s)" } else { "" })
482 .draw()?;
483
484 chart.draw_series(LineSeries::new(
485 t.iter().zip(series.iter()).map(|(&x, &y)| (x, y)),
486 &BLACK,
487 ))?;
488
489 if let Some((lb, ub)) = constraints_bounds[row] {
490 chart.draw_series(LineSeries::new(
491 vec![(t[0], lb), (t[t.len() - 1], lb)],
492 ShapeStyle::from(&RED.mix(0.5)).stroke_width(2).filled(),
493 ))?;
494 chart.draw_series(LineSeries::new(
495 vec![(t[0], ub), (t[t.len() - 1], ub)],
496 ShapeStyle::from(&RED.mix(0.5)).stroke_width(2).filled(),
497 ))?;
498 }
499 }
500 }
501
502 root.titled(title, ("sans-serif", 32))?;
503 root.present()?;
504 crate::verbosity_log!(crate::diag::Verbosity::Summary, "Saved plot: {filename}");
505 Ok(())
506 }
507
508 fn finite_diff_inplace(src: &DMatrix<f64>, dst: &mut DMatrix<f64>, h: f64) {
511 let n = src.ncols();
512 if n < 2 {
513 return;
514 }
515 let inv2h = 1.0 / (2.0 * h);
516 for (src_row, mut dst_row) in src.row_iter().zip(dst.row_iter_mut()) {
518 dst_row[0] = (src_row[1] - src_row[0]) / h;
520 dst_row[n - 1] = (src_row[n - 1] - src_row[n - 2]) / h;
522 for j in 1..n - 1 {
524 dst_row[j] = (src_row[j + 1] - src_row[j - 1]) * inv2h;
525 }
526 }
527 }
528
529 fn min_max(data: &[f64]) -> (f64, f64) {
530 data.iter()
531 .copied()
532 .filter(|v| v.is_finite())
533 .fold((f64::INFINITY, f64::NEG_INFINITY), |(mn, mx), v| {
534 (mn.min(v), mx.max(v))
535 })
536 }
537
538 #[test]
539 #[ignore = "plotting"]
540 fn test_topp3_with_parametric_path() -> Result<(), Box<dyn Error>> {
541 crate::verbosity_log!(
542 crate::diag::Verbosity::Summary,
543 "\n=== TOPP3 with Parametric Path (6-DOF) ==="
544 );
545
546 let start = Instant::now();
547 let path = make_parametric_path_6dof()?;
548 let build_ms = start.elapsed().as_secs_f64() * 1e3;
549
550 let s = make_s_vector(N);
551 let start = Instant::now();
552 let derivs = path.evaluate_up_to_3rd(s.as_slice())?;
553 let eval_ms = start.elapsed().as_secs_f64() * 1e3;
554
555 crate::verbosity_log!(
556 crate::diag::Verbosity::Summary,
557 "[Path Build] {build_ms:.3} ms, [Path Eval] {eval_ms:.3} ms"
558 );
559
560 let mut robot = setup_robot_with_constraints(&s, &derivs)?;
561 let s_slice: Vec<f64> = (0..N).map(|j| s[(0, j)]).collect();
562 let (a, b, num_stat, timings) = solve_topp_pipeline(&mut robot, &s_slice)?;
563
564 crate::verbosity_log!(
565 crate::diag::Verbosity::Summary,
566 "[TOPP2-RA] {:.3} ms, t_motion = {:.3} s",
567 timings.topp2_ms,
568 timings.t_motion_2
569 );
570 crate::verbosity_log!(
571 crate::diag::Verbosity::Summary,
572 "[TOPP3-LP] {:.3} ms, t_motion = {:.3} s",
573 timings.topp3_ms,
574 timings.t_motion_3
575 );
576
577 let (t, q_t, dq_t, ddq_t, dddq_t, interp_ms) =
578 interpolate_to_time_domain(&path, &s_slice, &a, &b, num_stat, 1e-3)?;
579 crate::verbosity_log!(
580 crate::diag::Verbosity::Summary,
581 "[Interpolation] {:.3} ms, {} samples",
582 interp_ms,
583 t.len()
584 );
585
586 plot_topp3_grid(
587 "data/path_topp_plots/parametric_path_topp3.png",
588 "TOPP3 Result: Parametric Path (6-DOF)",
589 &t,
590 &q_t,
591 &dq_t,
592 &ddq_t,
593 &dddq_t,
594 )?;
595
596 Ok(())
597 }
598}