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