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