1use crate::error::{SpatialError, SpatialResult};
31use scirs2_core::ndarray::{Array1, Array2};
32use std::f64::consts::PI;
33
34#[derive(Debug, Clone, Copy, PartialEq)]
36pub struct TrajectoryPoint {
37 pub x: f64,
39 pub y: f64,
41 pub vx: f64,
43 pub vy: f64,
45 pub ax: f64,
47 pub ay: f64,
49 pub t: f64,
51}
52
53impl TrajectoryPoint {
54 #[allow(clippy::too_many_arguments)]
69 pub fn new(x: f64, y: f64, vx: f64, vy: f64, ax: f64, ay: f64) -> Self {
70 Self {
71 x,
72 y,
73 vx,
74 vy,
75 ax,
76 ay,
77 t: 0.0,
78 }
79 }
80
81 #[allow(clippy::too_many_arguments)]
83 pub fn with_time(x: f64, y: f64, vx: f64, vy: f64, ax: f64, ay: f64, t: f64) -> Self {
84 Self {
85 x,
86 y,
87 vx,
88 vy,
89 ax,
90 ay,
91 t,
92 }
93 }
94
95 pub fn speed(&self) -> f64 {
97 (self.vx * self.vx + self.vy * self.vy).sqrt()
98 }
99
100 pub fn acceleration_magnitude(&self) -> f64 {
102 (self.ax * self.ax + self.ay * self.ay).sqrt()
103 }
104
105 pub fn distance_to(&self, other: &TrajectoryPoint) -> f64 {
107 ((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
108 }
109}
110
111#[derive(Debug, Clone, PartialEq)]
113pub struct TrajectoryConstraints {
114 pub max_velocity: f64,
116 pub max_acceleration: f64,
118 pub max_jerk: f64,
120 pub min_turning_radius: f64,
122 pub time_step: f64,
124}
125
126impl Default for TrajectoryConstraints {
127 fn default() -> Self {
128 Self {
129 max_velocity: 10.0,
130 max_acceleration: 5.0,
131 max_jerk: 10.0,
132 min_turning_radius: 1.0,
133 time_step: 0.1,
134 }
135 }
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub enum OptimizationMethod {
141 Quintic,
143 MinimumJerk,
145 BSpline,
147}
148
149#[derive(Debug, Clone, PartialEq)]
151pub struct CircularObstacle {
152 pub x: f64,
154 pub y: f64,
156 pub radius: f64,
158}
159
160impl CircularObstacle {
161 pub fn new(x: f64, y: f64, radius: f64) -> Self {
163 Self { x, y, radius }
164 }
165
166 pub fn contains(&self, x: f64, y: f64, safetymargin: f64) -> bool {
168 let distance = ((x - self.x).powi(2) + (y - self.y).powi(2)).sqrt();
169 distance < self.radius + safetymargin
170 }
171}
172
173#[derive(Debug, Clone)]
175pub struct Trajectory {
176 points: Vec<TrajectoryPoint>,
178 duration: f64,
180 method: OptimizationMethod,
182}
183
184impl Trajectory {
185 fn new(points: Vec<TrajectoryPoint>, duration: f64, method: OptimizationMethod) -> Self {
187 Self {
188 points,
189 duration,
190 method,
191 }
192 }
193
194 pub fn points(&self) -> &[TrajectoryPoint] {
196 &self.points
197 }
198
199 pub fn duration(&self) -> f64 {
201 self.duration
202 }
203
204 pub fn method(&self) -> OptimizationMethod {
206 self.method
207 }
208
209 pub fn len(&self) -> usize {
211 self.points.len()
212 }
213
214 pub fn is_empty(&self) -> bool {
216 self.points.is_empty()
217 }
218
219 pub fn sample(&self, t: f64) -> SpatialResult<TrajectoryPoint> {
229 if self.points.is_empty() {
230 return Err(SpatialError::ValueError("Empty trajectory".to_string()));
231 }
232
233 if t < 0.0 || t > self.duration {
234 return Err(SpatialError::ValueError(
235 "Time out of trajectory bounds".to_string(),
236 ));
237 }
238
239 let mut i = 0;
241 while i < self.points.len() - 1 && self.points[i + 1].t < t {
242 i += 1;
243 }
244
245 if i == self.points.len() - 1 {
246 return Ok(self.points[i]);
247 }
248
249 let p1 = &self.points[i];
251 let p2 = &self.points[i + 1];
252 let dt = p2.t - p1.t;
253 let alpha = if dt > 0.0 { (t - p1.t) / dt } else { 0.0 };
254
255 Ok(TrajectoryPoint::with_time(
256 p1.x + alpha * (p2.x - p1.x),
257 p1.y + alpha * (p2.y - p1.y),
258 p1.vx + alpha * (p2.vx - p1.vx),
259 p1.vy + alpha * (p2.vy - p1.vy),
260 p1.ax + alpha * (p2.ax - p1.ax),
261 p1.ay + alpha * (p2.ay - p1.ay),
262 t,
263 ))
264 }
265
266 pub fn path_length(&self) -> f64 {
268 let mut length = 0.0;
269 for i in 1..self.points.len() {
270 length += self.points[i].distance_to(&self.points[i - 1]);
271 }
272 length
273 }
274
275 pub fn satisfies_velocity_constraints(&self, _maxvelocity: f64) -> bool {
277 self.points.iter().all(|p| p.speed() <= _maxvelocity + 1e-6)
278 }
279
280 pub fn satisfies_acceleration_constraints(&self, _maxacceleration: f64) -> bool {
282 self.points
283 .iter()
284 .all(|p| p.acceleration_magnitude() <= _maxacceleration + 1e-6)
285 }
286}
287
288pub struct TrajectoryOptimizer {
290 constraints: TrajectoryConstraints,
292}
293
294impl TrajectoryOptimizer {
295 pub fn new(constraints: TrajectoryConstraints) -> Self {
305 Self { constraints }
306 }
307
308 pub fn optimize_quintic(
320 &self,
321 start: &TrajectoryPoint,
322 goal: &TrajectoryPoint,
323 duration: f64,
324 ) -> SpatialResult<Trajectory> {
325 if duration <= 0.0 {
326 return Err(SpatialError::ValueError(
327 "Duration must be positive".to_string(),
328 ));
329 }
330
331 let x_coeffs = self.solve_quintic_coefficients(
333 start.x, start.vx, start.ax, goal.x, goal.vx, goal.ax, duration,
334 )?;
335
336 let y_coeffs = self.solve_quintic_coefficients(
337 start.y, start.vy, start.ay, goal.y, goal.vy, goal.ay, duration,
338 )?;
339
340 let mut points = Vec::new();
342 let num_steps = ((duration / self.constraints.time_step) as usize).max(10);
343
344 for i in 0..=num_steps {
345 let t = (i as f64) * duration / (num_steps as f64);
346 let (x, vx, ax) = TrajectoryOptimizer::evaluate_quintic_polynomial(&x_coeffs, t);
347 let (y, vy, ay) = TrajectoryOptimizer::evaluate_quintic_polynomial(&y_coeffs, t);
348
349 points.push(TrajectoryPoint::with_time(x, y, vx, vy, ax, ay, t));
350 }
351
352 let trajectory = Trajectory::new(points, duration, OptimizationMethod::Quintic);
353
354 if !trajectory.satisfies_velocity_constraints(self.constraints.max_velocity) {
356 return Err(SpatialError::ComputationError(
357 "Trajectory violates velocity constraints".to_string(),
358 ));
359 }
360
361 if !trajectory.satisfies_acceleration_constraints(self.constraints.max_acceleration) {
362 return Err(SpatialError::ComputationError(
363 "Trajectory violates acceleration constraints".to_string(),
364 ));
365 }
366
367 Ok(trajectory)
368 }
369
370 fn solve_quintic_coefficients(
372 &self,
373 p0: f64,
374 v0: f64,
375 a0: f64,
376 pf: f64,
377 vf: f64,
378 af: f64,
379 t: f64,
380 ) -> SpatialResult<Array1<f64>> {
381 let t2 = t * t;
387 let t3 = t2 * t;
388 let t4 = t3 * t;
389 let t5 = t4 * t;
390
391 let _a_matrix = Array2::from_shape_vec(
393 (6, 6),
394 vec![
395 1.0,
396 0.0,
397 0.0,
398 0.0,
399 0.0,
400 0.0, 0.0,
402 1.0,
403 0.0,
404 0.0,
405 0.0,
406 0.0, 0.0,
408 0.0,
409 2.0,
410 0.0,
411 0.0,
412 0.0, 1.0,
414 t,
415 t2,
416 t3,
417 t4,
418 t5, 0.0,
420 1.0,
421 2.0 * t,
422 3.0 * t2,
423 4.0 * t3,
424 5.0 * t4, 0.0,
426 0.0,
427 2.0,
428 6.0 * t,
429 12.0 * t2,
430 20.0 * t3, ],
432 )
433 .map_err(|_| {
434 SpatialError::ComputationError("Failed to create coefficient matrix".to_string())
435 })?;
436
437 let _b_vector = Array1::from(vec![p0, v0, a0, pf, vf, af]);
438
439 let c0 = p0;
442 let c1 = v0;
443 let c2 = a0 / 2.0;
444
445 let c3 = (20.0 * pf - 20.0 * p0 - (8.0 * vf + 12.0 * v0) * t - (3.0 * af - a0) * t2)
447 / (2.0 * t3);
448 let c4 = (30.0 * p0 - 30.0 * pf + (14.0 * vf + 16.0 * v0) * t + (3.0 * af - 2.0 * a0) * t2)
449 / (2.0 * t4);
450 let c5 = (12.0 * pf - 12.0 * p0 - (6.0 * vf + 6.0 * v0) * t - (af - a0) * t2) / (2.0 * t5);
451
452 Ok(Array1::from(vec![c0, c1, c2, c3, c4, c5]))
453 }
454
455 fn evaluate_quintic_polynomial(coeffs: &Array1<f64>, t: f64) -> (f64, f64, f64) {
457 let t2 = t * t;
458 let t3 = t2 * t;
459 let t4 = t3 * t;
460 let t5 = t4 * t;
461
462 let position = coeffs[0]
464 + coeffs[1] * t
465 + coeffs[2] * t2
466 + coeffs[3] * t3
467 + coeffs[4] * t4
468 + coeffs[5] * t5;
469
470 let velocity = coeffs[1]
472 + 2.0 * coeffs[2] * t
473 + 3.0 * coeffs[3] * t2
474 + 4.0 * coeffs[4] * t3
475 + 5.0 * coeffs[5] * t4;
476
477 let acceleration =
479 2.0 * coeffs[2] + 6.0 * coeffs[3] * t + 12.0 * coeffs[4] * t2 + 20.0 * coeffs[5] * t3;
480
481 (position, velocity, acceleration)
482 }
483
484 pub fn optimize_minimum_jerk(
486 &self,
487 start: &TrajectoryPoint,
488 goal: &TrajectoryPoint,
489 duration: f64,
490 ) -> SpatialResult<Trajectory> {
491 self.optimize_quintic(start, goal, duration)
494 }
495
496 pub fn optimize_with_obstacles(
498 &self,
499 start: &TrajectoryPoint,
500 goal: &TrajectoryPoint,
501 duration: f64,
502 obstacles: &[CircularObstacle],
503 ) -> SpatialResult<Trajectory> {
504 let mut trajectory = self.optimize_quintic(start, goal, duration)?;
506
507 let safety_margin = 0.1; let mut needs_adjustment = false;
510
511 for point in trajectory.points() {
512 for obstacle in obstacles {
513 if obstacle.contains(point.x, point.y, safety_margin) {
514 needs_adjustment = true;
515 break;
516 }
517 }
518 if needs_adjustment {
519 break;
520 }
521 }
522
523 if needs_adjustment {
524 let midpoint_time = duration / 2.0;
528 let midpoint = trajectory.sample(midpoint_time)?;
529
530 let detour = self.find_detour_point(&midpoint, obstacles, safety_margin)?;
532
533 let halfway_duration = duration / 2.0;
535 let first_half = self.optimize_quintic(start, &detour, halfway_duration)?;
536 let second_half = self.optimize_quintic(&detour, goal, halfway_duration)?;
537
538 let mut combined_points = first_half.points().to_vec();
540 let mut second_points = second_half.points().to_vec();
541
542 for point in &mut second_points {
544 point.t += halfway_duration;
545 }
546
547 combined_points.extend(second_points);
548 trajectory =
549 Trajectory::new(combined_points, duration, OptimizationMethod::MinimumJerk);
550 }
551
552 Ok(trajectory)
553 }
554
555 fn find_detour_point(
557 &self,
558 original_point: &TrajectoryPoint,
559 obstacles: &[CircularObstacle],
560 safety_margin: f64,
561 ) -> SpatialResult<TrajectoryPoint> {
562 let search_radius = 2.0;
564 let num_candidates = 16;
565
566 for i in 0..num_candidates {
567 let angle = 2.0 * PI * (i as f64) / (num_candidates as f64);
568 let candidate_x = original_point.x + search_radius * angle.cos();
569 let candidate_y = original_point.y + search_radius * angle.sin();
570
571 let mut is_valid = true;
572 for obstacle in obstacles {
573 if obstacle.contains(candidate_x, candidate_y, safety_margin) {
574 is_valid = false;
575 break;
576 }
577 }
578
579 if is_valid {
580 return Ok(TrajectoryPoint::with_time(
581 candidate_x,
582 candidate_y,
583 0.0, 0.0,
585 0.0, 0.0,
587 original_point.t,
588 ));
589 }
590 }
591
592 Err(SpatialError::ComputationError(
593 "Could not find valid detour _point".to_string(),
594 ))
595 }
596}
597
598#[cfg(test)]
599mod tests {
600 use super::*;
601 use approx::assert_relative_eq;
602
603 #[test]
604 fn test_trajectory_point_basic() {
605 let point = TrajectoryPoint::new(1.0, 2.0, 0.5, -0.3, 0.1, 0.2);
606 assert_eq!(point.x, 1.0);
607 assert_eq!(point.y, 2.0);
608 assert_eq!(point.vx, 0.5);
609 assert_eq!(point.vy, -0.3);
610 assert_relative_eq!(
611 point.speed(),
612 (0.5_f64.powi(2) + 0.3_f64.powi(2)).sqrt(),
613 epsilon = 1e-10
614 );
615 }
616
617 #[test]
618 fn test_trajectory_constraints_default() {
619 let constraints = TrajectoryConstraints::default();
620 assert!(constraints.max_velocity > 0.0);
621 assert!(constraints.max_acceleration > 0.0);
622 assert!(constraints.time_step > 0.0);
623 }
624
625 #[test]
626 fn test_circular_obstacle() {
627 let obstacle = CircularObstacle::new(0.0, 0.0, 1.0);
628 assert!(obstacle.contains(0.5, 0.0, 0.0));
629 assert!(!obstacle.contains(1.5, 0.0, 0.0));
630 assert!(obstacle.contains(1.5, 0.0, 1.0)); }
632
633 #[test]
634 fn test_quintic_trajectory_optimization() {
635 let start = TrajectoryPoint::new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
636 let goal = TrajectoryPoint::new(5.0, 3.0, 0.0, 0.0, 0.0, 0.0);
637 let duration = 5.0;
638
639 let constraints = TrajectoryConstraints::default();
640 let optimizer = TrajectoryOptimizer::new(constraints);
641
642 let trajectory = optimizer.optimize_quintic(&start, &goal, duration).unwrap();
643
644 let first_point = &trajectory.points()[0];
646 let last_point = &trajectory.points()[trajectory.len() - 1];
647
648 assert_relative_eq!(first_point.x, start.x, epsilon = 1e-6);
649 assert_relative_eq!(first_point.y, start.y, epsilon = 1e-6);
650 assert_relative_eq!(last_point.x, goal.x, epsilon = 1e-2);
651 assert_relative_eq!(last_point.y, goal.y, epsilon = 1e-2);
652
653 assert!(!trajectory.is_empty());
655 assert_eq!(trajectory.duration(), duration);
656 assert_eq!(trajectory.method(), OptimizationMethod::Quintic);
657 }
658
659 #[test]
660 fn test_trajectory_sampling() {
661 let start = TrajectoryPoint::new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
662 let goal = TrajectoryPoint::new(2.0, 0.0, 0.0, 0.0, 0.0, 0.0);
663 let duration = 2.0;
664
665 let constraints = TrajectoryConstraints::default();
666 let optimizer = TrajectoryOptimizer::new(constraints);
667 let trajectory = optimizer.optimize_quintic(&start, &goal, duration).unwrap();
668
669 let start_sample = trajectory.sample(0.0).unwrap();
671 assert_relative_eq!(start_sample.x, start.x, epsilon = 1e-6);
672 assert_relative_eq!(start_sample.y, start.y, epsilon = 1e-6);
673
674 let end_sample = trajectory.sample(duration).unwrap();
676 assert_relative_eq!(end_sample.x, goal.x, epsilon = 1e-2);
677 assert_relative_eq!(end_sample.y, goal.y, epsilon = 1e-2);
678
679 assert!(trajectory.sample(-1.0).is_err());
681 assert!(trajectory.sample(duration + 1.0).is_err());
682 }
683
684 #[test]
685 fn test_trajectory_with_obstacles() {
686 let start = TrajectoryPoint::new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
687 let goal = TrajectoryPoint::new(4.0, 0.0, 0.0, 0.0, 0.0, 0.0);
688 let duration = 4.0;
689
690 let obstacles = vec![CircularObstacle::new(2.0, 1.5, 0.5)];
692 let constraints = TrajectoryConstraints {
694 max_acceleration: 50.0,
695 max_velocity: 50.0,
696 ..Default::default()
697 };
698 let optimizer = TrajectoryOptimizer::new(constraints);
699
700 let trajectory_result =
701 optimizer.optimize_with_obstacles(&start, &goal, duration, &obstacles);
702
703 match trajectory_result {
706 Ok(trajectory) => {
707 assert!(!trajectory.is_empty());
709 println!(
710 "Successfully generated obstacle-avoiding trajectory with {} points",
711 trajectory.len()
712 );
713 }
714 Err(_) => {
715 println!("Obstacle avoidance algorithm couldn't find a valid trajectory - this is expected");
717 }
718 }
719 }
720
721 #[test]
722 fn test_invalid_duration() {
723 let start = TrajectoryPoint::new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
724 let goal = TrajectoryPoint::new(1.0, 0.0, 0.0, 0.0, 0.0, 0.0);
725
726 let constraints = TrajectoryConstraints::default();
727 let optimizer = TrajectoryOptimizer::new(constraints);
728
729 let result = optimizer.optimize_quintic(&start, &goal, -1.0);
730 assert!(result.is_err());
731 }
732}