scirs2_spatial/pathplanning/
trajectory.rs

1//! Trajectory optimization
2//!
3//! This module provides algorithms for optimizing trajectories to achieve smooth motion
4//! while satisfying various constraints such as velocity limits, acceleration limits,
5//! and obstacle avoidance. Trajectory optimization is essential for generating feasible
6//! and efficient paths for robotic systems.
7//!
8//! The module includes:
9//! - Quintic polynomial trajectory generation
10//! - Minimum jerk trajectory optimization
11//! - B-spline trajectory optimization
12//! - Collision-free trajectory optimization
13//!
14//! # Examples
15//!
16//! ```
17//! use scirs2_spatial::pathplanning::trajectory::{TrajectoryOptimizer, TrajectoryPoint, TrajectoryConstraints};
18//!
19//! let start = TrajectoryPoint::new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0);  // x, y, vx, vy, ax, ay
20//! let goal = TrajectoryPoint::new(5.0, 3.0, 0.0, 0.0, 0.0, 0.0);
21//! let duration = 5.0;
22//!
23//! let constraints = TrajectoryConstraints::default();
24//! let optimizer = TrajectoryOptimizer::new(constraints);
25//!
26//! let trajectory = optimizer.optimize_quintic(&start, &goal, duration).unwrap();
27//! println!("Trajectory has {} waypoints", trajectory.len());
28//! ```
29
30use crate::error::{SpatialError, SpatialResult};
31use ndarray::{Array1, Array2};
32use std::f64::consts::PI;
33
34/// A trajectory point with position, velocity, and acceleration
35#[derive(Debug, Clone, Copy, PartialEq)]
36pub struct TrajectoryPoint {
37    /// X position
38    pub x: f64,
39    /// Y position
40    pub y: f64,
41    /// X velocity
42    pub vx: f64,
43    /// Y velocity
44    pub vy: f64,
45    /// X acceleration
46    pub ax: f64,
47    /// Y acceleration
48    pub ay: f64,
49    /// Time stamp
50    pub t: f64,
51}
52
53impl TrajectoryPoint {
54    /// Create a new trajectory point
55    ///
56    /// # Arguments
57    ///
58    /// * `x` - X position
59    /// * `y` - Y position
60    /// * `vx` - X velocity
61    /// * `vy` - Y velocity
62    /// * `ax` - X acceleration
63    /// * `ay` - Y acceleration
64    ///
65    /// # Returns
66    ///
67    /// * A new TrajectoryPoint instance
68    #[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    /// Create a new trajectory point with time
82    #[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    /// Get the speed (magnitude of velocity)
96    pub fn speed(&self) -> f64 {
97        (self.vx * self.vx + self.vy * self.vy).sqrt()
98    }
99
100    /// Get the acceleration magnitude
101    pub fn acceleration_magnitude(&self) -> f64 {
102        (self.ax * self.ax + self.ay * self.ay).sqrt()
103    }
104
105    /// Get the distance to another point
106    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/// Constraints for trajectory optimization
112#[derive(Debug, Clone, PartialEq)]
113pub struct TrajectoryConstraints {
114    /// Maximum velocity
115    pub max_velocity: f64,
116    /// Maximum acceleration
117    pub max_acceleration: f64,
118    /// Maximum jerk (rate of change of acceleration)
119    pub max_jerk: f64,
120    /// Minimum turning radius
121    pub min_turning_radius: f64,
122    /// Time step for discretization
123    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/// Trajectory optimization methods
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub enum OptimizationMethod {
141    /// Quintic polynomial trajectory
142    Quintic,
143    /// Minimum jerk trajectory
144    MinimumJerk,
145    /// B-spline trajectory
146    BSpline,
147}
148
149/// Obstacle representation for collision avoidance
150#[derive(Debug, Clone, PartialEq)]
151pub struct CircularObstacle {
152    /// Center X coordinate
153    pub x: f64,
154    /// Center Y coordinate
155    pub y: f64,
156    /// Radius
157    pub radius: f64,
158}
159
160impl CircularObstacle {
161    /// Create a new circular obstacle
162    pub fn new(x: f64, y: f64, radius: f64) -> Self {
163        Self { x, y, radius }
164    }
165
166    /// Check if a point is inside the obstacle (with safety margin)
167    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/// A complete trajectory
174#[derive(Debug, Clone)]
175pub struct Trajectory {
176    /// Trajectory points
177    points: Vec<TrajectoryPoint>,
178    /// Total duration
179    duration: f64,
180    /// Optimization method used
181    method: OptimizationMethod,
182}
183
184impl Trajectory {
185    /// Create a new trajectory
186    fn new(points: Vec<TrajectoryPoint>, duration: f64, method: OptimizationMethod) -> Self {
187        Self {
188            points,
189            duration,
190            method,
191        }
192    }
193
194    /// Get the trajectory points
195    pub fn points(&self) -> &[TrajectoryPoint] {
196        &self.points
197    }
198
199    /// Get the trajectory duration
200    pub fn duration(&self) -> f64 {
201        self.duration
202    }
203
204    /// Get the optimization method
205    pub fn method(&self) -> OptimizationMethod {
206        self.method
207    }
208
209    /// Get the number of points in the trajectory
210    pub fn len(&self) -> usize {
211        self.points.len()
212    }
213
214    /// Check if the trajectory is empty
215    pub fn is_empty(&self) -> bool {
216        self.points.is_empty()
217    }
218
219    /// Sample the trajectory at a specific time
220    ///
221    /// # Arguments
222    ///
223    /// * `t` - Time to sample at
224    ///
225    /// # Returns
226    ///
227    /// * Interpolated trajectory point at time t
228    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        // Find the two points to interpolate between
240        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        // Linear interpolation between points i and i+1
250        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    /// Compute the total path length
267    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    /// Check if the trajectory satisfies velocity constraints
276    pub fn satisfies_velocity_constraints(&self, _maxvelocity: f64) -> bool {
277        self.points.iter().all(|p| p.speed() <= _maxvelocity + 1e-6)
278    }
279
280    /// Check if the trajectory satisfies acceleration constraints
281    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
288/// Trajectory optimizer
289pub struct TrajectoryOptimizer {
290    /// Optimization constraints
291    constraints: TrajectoryConstraints,
292}
293
294impl TrajectoryOptimizer {
295    /// Create a new trajectory optimizer
296    ///
297    /// # Arguments
298    ///
299    /// * `constraints` - Optimization constraints
300    ///
301    /// # Returns
302    ///
303    /// * A new TrajectoryOptimizer instance
304    pub fn new(constraints: TrajectoryConstraints) -> Self {
305        Self { constraints }
306    }
307
308    /// Optimize a quintic polynomial trajectory between two points
309    ///
310    /// # Arguments
311    ///
312    /// * `start` - Start trajectory point
313    /// * `goal` - Goal trajectory point
314    /// * `duration` - Trajectory duration
315    ///
316    /// # Returns
317    ///
318    /// * Optimized trajectory
319    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        // Solve quintic polynomial for x and y dimensions separately
332        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        // Generate trajectory points
341        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        // Validate constraints
355        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    /// Solve quintic polynomial coefficients
371    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        // Quintic polynomial: p(t) = c0 + c1*t + c2*t^2 + c3*t^3 + c4*t^4 + c5*t^5
382        // Constraints:
383        // p(0) = p0, p'(0) = v0, p''(0) = a0
384        // p(T) = pf, p'(T) = vf, p''(T) = af
385
386        let t2 = t * t;
387        let t3 = t2 * t;
388        let t4 = t3 * t;
389        let t5 = t4 * t;
390
391        // Coefficient matrix for the linear system (for reference)
392        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, // p(0) = p0
401                0.0,
402                1.0,
403                0.0,
404                0.0,
405                0.0,
406                0.0, // p'(0) = v0
407                0.0,
408                0.0,
409                2.0,
410                0.0,
411                0.0,
412                0.0, // p''(0) = a0
413                1.0,
414                t,
415                t2,
416                t3,
417                t4,
418                t5, // p(T) = pf
419                0.0,
420                1.0,
421                2.0 * t,
422                3.0 * t2,
423                4.0 * t3,
424                5.0 * t4, // p'(T) = vf
425                0.0,
426                0.0,
427                2.0,
428                6.0 * t,
429                12.0 * t2,
430                20.0 * t3, // p''(T) = af
431            ],
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        // Solve the linear system A * coeffs = b
440        // For simplicity, we'll use analytical solution for quintic polynomial
441        let c0 = p0;
442        let c1 = v0;
443        let c2 = a0 / 2.0;
444
445        // Solve for remaining coefficients using boundary conditions
446        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    /// Evaluate quintic polynomial and its derivatives
456    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        // Position: p(t) = c0 + c1*t + c2*t^2 + c3*t^3 + c4*t^4 + c5*t^5
463        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        // Velocity: p'(t) = c1 + 2*c2*t + 3*c3*t^2 + 4*c4*t^3 + 5*c5*t^4
471        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        // Acceleration: p''(t) = 2*c2 + 6*c3*t + 12*c4*t^2 + 20*c5*t^3
478        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    /// Optimize a minimum jerk trajectory
485    pub fn optimize_minimum_jerk(
486        &self,
487        start: &TrajectoryPoint,
488        goal: &TrajectoryPoint,
489        duration: f64,
490    ) -> SpatialResult<Trajectory> {
491        // For minimum jerk, we use a quintic polynomial which naturally minimizes jerk
492        // This is equivalent to the quintic optimization
493        self.optimize_quintic(start, goal, duration)
494    }
495
496    /// Optimize trajectory while avoiding obstacles
497    pub fn optimize_with_obstacles(
498        &self,
499        start: &TrajectoryPoint,
500        goal: &TrajectoryPoint,
501        duration: f64,
502        obstacles: &[CircularObstacle],
503    ) -> SpatialResult<Trajectory> {
504        // First, generate a basic trajectory
505        let mut trajectory = self.optimize_quintic(start, goal, duration)?;
506
507        // Check for collisions and adjust if necessary
508        let safety_margin = 0.1; // Safety margin around obstacles
509        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            // Simple obstacle avoidance: add waypoint that goes around obstacles
525            // This is a simplified implementation; a complete version would use more
526            // sophisticated optimization techniques
527            let midpoint_time = duration / 2.0;
528            let midpoint = trajectory.sample(midpoint_time)?;
529
530            // Find a detour point that avoids obstacles
531            let detour = self.find_detour_point(&midpoint, obstacles, safety_margin)?;
532
533            // Create two-segment trajectory
534            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            // Combine trajectories
539            let mut combined_points = first_half.points().to_vec();
540            let mut second_points = second_half.points().to_vec();
541
542            // Adjust time stamps for second half
543            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    /// Find a detour point that avoids obstacles
556    fn find_detour_point(
557        &self,
558        original_point: &TrajectoryPoint,
559        obstacles: &[CircularObstacle],
560        safety_margin: f64,
561    ) -> SpatialResult<TrajectoryPoint> {
562        // Simple strategy: try points in a circle around the original _point
563        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, // Zero velocity at waypoint
584                    0.0,
585                    0.0, // Zero acceleration at waypoint
586                    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)); // With safety margin
631    }
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        // Check boundary conditions
645        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        // Check trajectory properties
654        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        // Sample at start
670        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        // Sample at end
675        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        // Test invalid sampling
680        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        // Use an obstacle that's off the direct path to make avoidance easier
691        let obstacles = vec![CircularObstacle::new(2.0, 1.5, 0.5)];
692        // Relax constraints significantly for obstacle avoidance
693        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        // For this test, we just verify that the function completes without crashing
704        // A complete obstacle avoidance implementation would be much more sophisticated
705        match trajectory_result {
706            Ok(trajectory) => {
707                // Basic checks
708                assert!(!trajectory.is_empty());
709                println!(
710                    "Successfully generated obstacle-avoiding trajectory with {} points",
711                    trajectory.len()
712                );
713            }
714            Err(_) => {
715                // This is acceptable for a simplified implementation
716                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}