1pub mod autodiff;
62mod path_core;
63pub mod spline;
64
65pub use autodiff::{Jet3, cos, exp, ln, powi, sin, sqrt};
66pub use path_core::{
67 ParametricFn, Path, PathDerivatives, PathEvaluator, PathEvaluator2nd, PathEvaluator3rd,
68};
69pub use spline::{Parametrization, SplineConfig};
70
71#[cfg(test)]
72use crate::diag::PathError;
73
74#[derive(Clone, Copy, Debug)]
76pub enum OutOfRangeMode {
77 Error,
79 Clamp,
81}
82
83#[cfg(test)]
88#[allow(clippy::type_complexity)]
89pub(crate) fn lissajous_path_for_test(
90 dim: usize,
91 s_len: usize,
92 rng: &mut impl rand::RngExt,
93) -> Result<(nalgebra::DMatrix<f64>, Path, Vec<f64>, Vec<f64>), PathError> {
94 let omega = (0..dim)
95 .map(|_| rng.random_range(0.1..(2.0 * std::f64::consts::PI)))
96 .collect::<Vec<f64>>();
97 let phi = (0..dim)
98 .map(|_| rng.random_range(0.0..(2.0 * std::f64::consts::PI)))
99 .collect::<Vec<f64>>();
100
101 let (s, path) = lissajous_path_fixed_for_test(dim, s_len, omega.clone(), phi.clone())?;
102
103 Ok((s, path, omega, phi))
104}
105
106#[cfg(test)]
111pub(crate) fn lissajous_path_fixed_for_test(
112 dim: usize,
113 s_len: usize,
114 omega: Vec<f64>,
115 phi: Vec<f64>,
116) -> Result<(nalgebra::DMatrix<f64>, Path), PathError> {
117 let s = nalgebra::DMatrix::<f64>::from_fn(1, s_len, |_, j| j as f64 / (s_len - 1) as f64);
118
119 let path = Path::from_parametric(
120 move |s: Jet3| {
121 (0..dim)
122 .map(|i| sin(omega[i] * s + phi[i]))
123 .collect::<Vec<Jet3>>()
124 },
125 0.0,
126 1.0,
127 )?;
128
129 Ok((s, path))
130}
131
132#[cfg(test)]
136pub(crate) fn add_symmetric_axial_limits_for_test<M: crate::robot::robot_core::RobotBasic>(
137 robot: &mut crate::robot::robot_core::Robot<M>,
138 vel_limit: f64,
139 acc_limit: f64,
140 jerk_limit: Option<f64>,
141) -> Result<(), crate::diag::ConstraintError> {
142 let dim = robot.dim();
143 let n = robot.constraints.len();
144 if n == 0 {
145 return Ok(());
146 }
147
148 let vel_max = vec![vel_limit; dim];
149 let vel_min = vec![-vel_limit; dim];
150
151 let acc_max = vec![acc_limit; dim];
152 let acc_min = vec![-acc_limit; dim];
153
154 let robot = robot
155 .with_axial_velocity((vel_max.as_slice(), n), (vel_min.as_slice(), n), 0)?
156 .with_axial_acceleration((acc_max.as_slice(), n), (acc_min.as_slice(), n), 0)?;
157
158 if let Some(jerk_limit) = jerk_limit {
159 let jerk_max = vec![jerk_limit; dim];
160 let jerk_min = vec![-jerk_limit; dim];
161 robot.with_axial_jerk((jerk_max.as_slice(), n), (jerk_min.as_slice(), n), 0)?;
162 }
163
164 Ok(())
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170 use crate::copp::copp2::stable::basic::{Topp2ProblemBuilder, s_to_t_topp2};
171 use crate::copp::copp2::stable::reach_set2::ReachSet2OptionsBuilder;
172 use crate::copp::copp2::stable::topp2_ra::topp2_ra;
173 use crate::copp::copp3::stable::basic::{
174 Topp3ProblemBuilder, Topp3Profile, s_to_t_topp3, t_to_s_topp3,
175 };
176 use crate::copp::copp3::stable::topp3_lp::topp3_lp;
177 use crate::copp::{ClarabelOptionsBuilder, InterpolationMode};
178 use crate::diag::ConstraintError;
179 use crate::diag::Verbosity;
180 use crate::robot::robot_core::Robot;
181 use nalgebra::DMatrix;
182 use plotters::prelude::*;
183 use rand::RngExt;
184 use std::error::Error;
185 use std::fs::create_dir_all;
186 use std::path::Path;
187 use std::time::Instant;
188
189 const DIM: usize = 6;
190 const N: usize = 1000;
191
192 #[test]
193 fn test_topp3_with_spline_path() -> Result<(), Box<dyn Error>> {
194 crate::verbosity_log!(
195 crate::diag::Verbosity::Summary,
196 "\n=== TOPP3 with Spline Path (6-DOF) ==="
197 );
198
199 let n_waypoints = 10;
200 let start = Instant::now();
201 let waypoints = make_random_waypoints(DIM, n_waypoints);
202 let sample_ms = start.elapsed().as_secs_f64() * 1e3;
203
204 let start = Instant::now();
205 let path = super::Path::from_waypoints(&waypoints, SplineConfig::default())?;
206 let build_ms = start.elapsed().as_secs_f64() * 1e3;
207
208 let s = make_s_vector(N);
209 let start = Instant::now();
210 let derivs = path.evaluate_up_to_3rd(s.as_slice())?;
211 let eval_ms = start.elapsed().as_secs_f64() * 1e3;
212
213 crate::verbosity_log!(
214 crate::diag::Verbosity::Summary,
215 "[Waypoint Sample] {sample_ms:.3} ms, [Spline Build] {build_ms:.3} ms, [Spline Eval] {eval_ms:.3} ms"
216 );
217
218 let mut robot = setup_robot_with_constraints(&s, &derivs)?;
219 let s_slice: Vec<f64> = (0..N).map(|j| s[(0, j)]).collect();
220 let (profile, timings) = solve_topp_pipeline(&mut robot, &s_slice)?;
221
222 crate::verbosity_log!(
223 crate::diag::Verbosity::Summary,
224 "[TOPP2-RA] {:.3} ms, t_motion = {:.3} s",
225 timings.topp2_ms,
226 timings.t_motion_2
227 );
228 crate::verbosity_log!(
229 crate::diag::Verbosity::Summary,
230 "[TOPP3-LP] {:.3} ms, t_motion = {:.3} s",
231 timings.topp3_ms,
232 timings.t_motion_3
233 );
234
235 let (t, q_t, dq_t, ddq_t, dddq_t, interp_ms) =
236 interpolate_to_time_domain(&path, &s_slice, &profile, 1e-3)?;
237 crate::verbosity_log!(
238 crate::diag::Verbosity::Summary,
239 "[Interpolation] {:.3} ms, {} samples",
240 interp_ms,
241 t.len()
242 );
243
244 plot_topp3_grid(
245 "data/path_topp_plots/spline_path_topp3.png",
246 "TOPP3 Result: Spline Path (6-DOF, 50 waypoints)",
247 &t,
248 &q_t,
249 &dq_t,
250 &ddq_t,
251 &dddq_t,
252 )?;
253
254 Ok(())
255 }
256
257 fn make_s_vector(n: usize) -> DMatrix<f64> {
258 DMatrix::<f64>::from_fn(1, n, |_, j| j as f64 / (n - 1) as f64)
259 }
260
261 fn make_parametric_path_6dof() -> Result<super::Path, PathError> {
262 super::Path::from_parametric(
263 |s: Jet3| {
264 vec![
265 sin(s),
266 cos(s),
267 exp(0.3 * s) - 1.0,
268 s + 0.1 * s * s - 0.01 * s * s * s * s,
269 sin(2.0 * s) + 0.15 * cos(3.0 * s),
270 sin(s) * cos(s),
271 ]
272 },
273 0.0,
274 1.0,
275 )
276 }
277
278 fn make_random_waypoints(dim: usize, n_pts: usize) -> DMatrix<f64> {
279 let mut rng = rand::rng();
280 let mut waypoints = DMatrix::<f64>::zeros(dim, n_pts);
283 for mut row in waypoints.row_iter_mut() {
284 row[0] = rng.random_range(-1.0..1.0);
285 for j in 1..n_pts {
286 let step = rng.random_range(-0.5..0.5);
287 row[j] = row[j - 1] + step;
288 }
289 }
290 waypoints
291 }
292
293 fn setup_robot_with_constraints(
294 s: &DMatrix<f64>,
295 derivs: &PathDerivatives,
296 ) -> Result<Robot<usize>, ConstraintError> {
297 let n = s.ncols();
298 let mut robot = Robot::with_capacity(DIM, n);
299
300 let vel_max = vec![1.0; DIM];
301 let vel_min = vec![-1.0; DIM];
302 let acc_max = vec![1.0; DIM];
303 let acc_min = vec![-1.0; DIM];
304 let jerk_max = vec![5.0; DIM];
305 let jerk_min = vec![-5.0; DIM];
306 robot
307 .with_s(&s.as_view())?
308 .with_q(
309 &derivs.q.as_view(),
310 &derivs.dq.as_ref().unwrap().as_view(),
311 &derivs.ddq.as_ref().unwrap().as_view(),
312 derivs.dddq.as_ref().map(|m| m.as_view()).as_ref(),
313 0,
314 )?
315 .with_axial_velocity((vel_max.as_slice(), n), (vel_min.as_slice(), n), 0)?
316 .with_axial_acceleration((acc_max.as_slice(), n), (acc_min.as_slice(), n), 0)?
317 .with_axial_jerk((jerk_max.as_slice(), n), (jerk_min.as_slice(), n), 0)?;
318
319 Ok(robot)
320 }
321
322 struct ToppTimings {
323 topp2_ms: f64,
324 topp3_ms: f64,
325 t_motion_2: f64,
326 t_motion_3: f64,
327 }
328
329 fn solve_topp_pipeline(
330 robot: &mut Robot<usize>,
331 s_slice: &[f64],
332 ) -> Result<(Topp3Profile, ToppTimings), Box<dyn Error>> {
333 let n = s_slice.len();
334
335 let topp2_problem = Topp2ProblemBuilder::new(robot, (0, n - 1), (0.0, 0.0)).build()?;
336
337 let options = ReachSet2OptionsBuilder::new()
338 .a_cmp_abs_tol(1e-9)
339 .a_cmp_rel_tol(1e-9)
340 .lp_feas_tol(1e-9)
341 .verbosity(Verbosity::Silent)
342 .build()?;
343
344 let start = Instant::now();
345 let a_ra = topp2_ra(&topp2_problem, &options)?;
346 let topp2_ms = start.elapsed().as_secs_f64() * 1e3;
347 let (t_motion_2, _) = s_to_t_topp2(s_slice, &a_ra, 0.0)?;
348
349 robot.constraints.amax_substitute(&a_ra, 0)?;
350
351 let topp3_problem = Topp3ProblemBuilder::new(robot, 0, &a_ra, (0.0, 0.0), (0.0, 0.0))
352 .with_num_stationary_max(2)
353 .build_with_linearization()?;
354 let options_lp = ClarabelOptionsBuilder::new()
355 .allow_almost_solved(true)
356 .build()?;
357
358 let start = Instant::now();
359 let profile = topp3_lp(&topp3_problem, &options_lp)?;
360 let topp3_ms = start.elapsed().as_secs_f64() * 1e3;
361 let (t_motion_3, _) = s_to_t_topp3(s_slice, profile.as_parts(), 0.0)?;
362
363 Ok((
364 profile,
365 ToppTimings {
366 topp2_ms,
367 topp3_ms,
368 t_motion_2,
369 t_motion_3,
370 },
371 ))
372 }
373
374 #[allow(clippy::type_complexity)]
375 fn interpolate_to_time_domain(
376 path: &super::Path,
377 s_slice: &[f64],
378 profile: &Topp3Profile,
379 dt: f64,
380 ) -> Result<
381 (
382 Vec<f64>,
383 DMatrix<f64>,
384 DMatrix<f64>,
385 DMatrix<f64>,
386 DMatrix<f64>,
387 f64,
388 ),
389 Box<dyn Error>,
390 > {
391 let start = Instant::now();
392 let (t_final, t_s) = s_to_t_topp3(s_slice, profile.as_parts(), 0.0)?;
393
394 let t_sample: Vec<f64> = (0..=((t_final / dt).floor() as usize))
395 .map(|i| i as f64 * dt)
396 .collect();
397
398 let s_t = t_to_s_topp3(
399 s_slice,
400 profile.as_parts(),
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 && !parent.as_os_str().is_empty()
436 {
437 create_dir_all(parent)?;
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 fn test_topp3_with_parametric_path() -> Result<(), Box<dyn Error>> {
540 crate::verbosity_log!(
541 crate::diag::Verbosity::Summary,
542 "\n=== TOPP3 with Parametric Path (6-DOF) ==="
543 );
544
545 let start = Instant::now();
546 let path = make_parametric_path_6dof()?;
547 let build_ms = start.elapsed().as_secs_f64() * 1e3;
548
549 let s = make_s_vector(N);
550 let start = Instant::now();
551 let derivs = path.evaluate_up_to_3rd(s.as_slice())?;
552 let eval_ms = start.elapsed().as_secs_f64() * 1e3;
553
554 crate::verbosity_log!(
555 crate::diag::Verbosity::Summary,
556 "[Path Build] {build_ms:.3} ms, [Path Eval] {eval_ms:.3} ms"
557 );
558
559 let mut robot = setup_robot_with_constraints(&s, &derivs)?;
560 let s_slice: Vec<f64> = (0..N).map(|j| s[(0, j)]).collect();
561 let (profile, timings) = solve_topp_pipeline(&mut robot, &s_slice)?;
562
563 crate::verbosity_log!(
564 crate::diag::Verbosity::Summary,
565 "[TOPP2-RA] {:.3} ms, t_motion = {:.3} s",
566 timings.topp2_ms,
567 timings.t_motion_2
568 );
569 crate::verbosity_log!(
570 crate::diag::Verbosity::Summary,
571 "[TOPP3-LP] {:.3} ms, t_motion = {:.3} s",
572 timings.topp3_ms,
573 timings.t_motion_3
574 );
575
576 let (t, q_t, dq_t, ddq_t, dddq_t, interp_ms) =
577 interpolate_to_time_domain(&path, &s_slice, &profile, 1e-3)?;
578 crate::verbosity_log!(
579 crate::diag::Verbosity::Summary,
580 "[Interpolation] {:.3} ms, {} samples",
581 interp_ms,
582 t.len()
583 );
584
585 plot_topp3_grid(
586 "data/path_topp_plots/parametric_path_topp3.png",
587 "TOPP3 Result: Parametric Path (6-DOF)",
588 &t,
589 &q_t,
590 &dq_t,
591 &ddq_t,
592 &dddq_t,
593 )?;
594
595 Ok(())
596 }
597}