use crate::error::{SpatialError, SpatialResult};
use scirs2_core::ndarray::{Array1, Array2};
use std::f64::consts::PI;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TrajectoryPoint {
pub x: f64,
pub y: f64,
pub vx: f64,
pub vy: f64,
pub ax: f64,
pub ay: f64,
pub t: f64,
}
impl TrajectoryPoint {
#[allow(clippy::too_many_arguments)]
pub fn new(x: f64, y: f64, vx: f64, vy: f64, ax: f64, ay: f64) -> Self {
Self {
x,
y,
vx,
vy,
ax,
ay,
t: 0.0,
}
}
#[allow(clippy::too_many_arguments)]
pub fn with_time(x: f64, y: f64, vx: f64, vy: f64, ax: f64, ay: f64, t: f64) -> Self {
Self {
x,
y,
vx,
vy,
ax,
ay,
t,
}
}
pub fn speed(&self) -> f64 {
(self.vx * self.vx + self.vy * self.vy).sqrt()
}
pub fn acceleration_magnitude(&self) -> f64 {
(self.ax * self.ax + self.ay * self.ay).sqrt()
}
pub fn distance_to(&self, other: &TrajectoryPoint) -> f64 {
((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TrajectoryConstraints {
pub max_velocity: f64,
pub max_acceleration: f64,
pub max_jerk: f64,
pub min_turning_radius: f64,
pub time_step: f64,
}
impl Default for TrajectoryConstraints {
fn default() -> Self {
Self {
max_velocity: 10.0,
max_acceleration: 5.0,
max_jerk: 10.0,
min_turning_radius: 1.0,
time_step: 0.1,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OptimizationMethod {
Quintic,
MinimumJerk,
BSpline,
}
#[derive(Debug, Clone, PartialEq)]
pub struct CircularObstacle {
pub x: f64,
pub y: f64,
pub radius: f64,
}
impl CircularObstacle {
pub fn new(x: f64, y: f64, radius: f64) -> Self {
Self { x, y, radius }
}
pub fn contains(&self, x: f64, y: f64, safetymargin: f64) -> bool {
let distance = ((x - self.x).powi(2) + (y - self.y).powi(2)).sqrt();
distance < self.radius + safetymargin
}
}
#[derive(Debug, Clone)]
pub struct Trajectory {
points: Vec<TrajectoryPoint>,
duration: f64,
method: OptimizationMethod,
}
impl Trajectory {
fn new(points: Vec<TrajectoryPoint>, duration: f64, method: OptimizationMethod) -> Self {
Self {
points,
duration,
method,
}
}
pub fn points(&self) -> &[TrajectoryPoint] {
&self.points
}
pub fn duration(&self) -> f64 {
self.duration
}
pub fn method(&self) -> OptimizationMethod {
self.method
}
pub fn len(&self) -> usize {
self.points.len()
}
pub fn is_empty(&self) -> bool {
self.points.is_empty()
}
pub fn sample(&self, t: f64) -> SpatialResult<TrajectoryPoint> {
if self.points.is_empty() {
return Err(SpatialError::ValueError("Empty trajectory".to_string()));
}
if t < 0.0 || t > self.duration {
return Err(SpatialError::ValueError(
"Time out of trajectory bounds".to_string(),
));
}
let mut i = 0;
while i < self.points.len() - 1 && self.points[i + 1].t < t {
i += 1;
}
if i == self.points.len() - 1 {
return Ok(self.points[i]);
}
let p1 = &self.points[i];
let p2 = &self.points[i + 1];
let dt = p2.t - p1.t;
let alpha = if dt > 0.0 { (t - p1.t) / dt } else { 0.0 };
Ok(TrajectoryPoint::with_time(
p1.x + alpha * (p2.x - p1.x),
p1.y + alpha * (p2.y - p1.y),
p1.vx + alpha * (p2.vx - p1.vx),
p1.vy + alpha * (p2.vy - p1.vy),
p1.ax + alpha * (p2.ax - p1.ax),
p1.ay + alpha * (p2.ay - p1.ay),
t,
))
}
pub fn path_length(&self) -> f64 {
let mut length = 0.0;
for i in 1..self.points.len() {
length += self.points[i].distance_to(&self.points[i - 1]);
}
length
}
pub fn satisfies_velocity_constraints(&self, _maxvelocity: f64) -> bool {
self.points.iter().all(|p| p.speed() <= _maxvelocity + 1e-6)
}
pub fn satisfies_acceleration_constraints(&self, _maxacceleration: f64) -> bool {
self.points
.iter()
.all(|p| p.acceleration_magnitude() <= _maxacceleration + 1e-6)
}
}
pub struct TrajectoryOptimizer {
constraints: TrajectoryConstraints,
}
impl TrajectoryOptimizer {
pub fn new(constraints: TrajectoryConstraints) -> Self {
Self { constraints }
}
pub fn optimize_quintic(
&self,
start: &TrajectoryPoint,
goal: &TrajectoryPoint,
duration: f64,
) -> SpatialResult<Trajectory> {
if duration <= 0.0 {
return Err(SpatialError::ValueError(
"Duration must be positive".to_string(),
));
}
let x_coeffs = self.solve_quintic_coefficients(
start.x, start.vx, start.ax, goal.x, goal.vx, goal.ax, duration,
)?;
let y_coeffs = self.solve_quintic_coefficients(
start.y, start.vy, start.ay, goal.y, goal.vy, goal.ay, duration,
)?;
let mut points = Vec::new();
let num_steps = ((duration / self.constraints.time_step) as usize).max(10);
for i in 0..=num_steps {
let t = (i as f64) * duration / (num_steps as f64);
let (x, vx, ax) = TrajectoryOptimizer::evaluate_quintic_polynomial(&x_coeffs, t);
let (y, vy, ay) = TrajectoryOptimizer::evaluate_quintic_polynomial(&y_coeffs, t);
points.push(TrajectoryPoint::with_time(x, y, vx, vy, ax, ay, t));
}
let trajectory = Trajectory::new(points, duration, OptimizationMethod::Quintic);
if !trajectory.satisfies_velocity_constraints(self.constraints.max_velocity) {
return Err(SpatialError::ComputationError(
"Trajectory violates velocity constraints".to_string(),
));
}
if !trajectory.satisfies_acceleration_constraints(self.constraints.max_acceleration) {
return Err(SpatialError::ComputationError(
"Trajectory violates acceleration constraints".to_string(),
));
}
Ok(trajectory)
}
fn solve_quintic_coefficients(
&self,
p0: f64,
v0: f64,
a0: f64,
pf: f64,
vf: f64,
af: f64,
t: f64,
) -> SpatialResult<Array1<f64>> {
let t2 = t * t;
let t3 = t2 * t;
let t4 = t3 * t;
let t5 = t4 * t;
let _a_matrix = Array2::from_shape_vec(
(6, 6),
vec![
1.0,
0.0,
0.0,
0.0,
0.0,
0.0, 0.0,
1.0,
0.0,
0.0,
0.0,
0.0, 0.0,
0.0,
2.0,
0.0,
0.0,
0.0, 1.0,
t,
t2,
t3,
t4,
t5, 0.0,
1.0,
2.0 * t,
3.0 * t2,
4.0 * t3,
5.0 * t4, 0.0,
0.0,
2.0,
6.0 * t,
12.0 * t2,
20.0 * t3, ],
)
.map_err(|_| {
SpatialError::ComputationError("Failed to create coefficient matrix".to_string())
})?;
let _b_vector = Array1::from(vec![p0, v0, a0, pf, vf, af]);
let c0 = p0;
let c1 = v0;
let c2 = a0 / 2.0;
let c3 = (20.0 * pf - 20.0 * p0 - (8.0 * vf + 12.0 * v0) * t - (3.0 * af - a0) * t2)
/ (2.0 * t3);
let c4 = (30.0 * p0 - 30.0 * pf + (14.0 * vf + 16.0 * v0) * t + (3.0 * af - 2.0 * a0) * t2)
/ (2.0 * t4);
let c5 = (12.0 * pf - 12.0 * p0 - (6.0 * vf + 6.0 * v0) * t - (af - a0) * t2) / (2.0 * t5);
Ok(Array1::from(vec![c0, c1, c2, c3, c4, c5]))
}
fn evaluate_quintic_polynomial(coeffs: &Array1<f64>, t: f64) -> (f64, f64, f64) {
let t2 = t * t;
let t3 = t2 * t;
let t4 = t3 * t;
let t5 = t4 * t;
let position = coeffs[0]
+ coeffs[1] * t
+ coeffs[2] * t2
+ coeffs[3] * t3
+ coeffs[4] * t4
+ coeffs[5] * t5;
let velocity = coeffs[1]
+ 2.0 * coeffs[2] * t
+ 3.0 * coeffs[3] * t2
+ 4.0 * coeffs[4] * t3
+ 5.0 * coeffs[5] * t4;
let acceleration =
2.0 * coeffs[2] + 6.0 * coeffs[3] * t + 12.0 * coeffs[4] * t2 + 20.0 * coeffs[5] * t3;
(position, velocity, acceleration)
}
pub fn optimize_minimum_jerk(
&self,
start: &TrajectoryPoint,
goal: &TrajectoryPoint,
duration: f64,
) -> SpatialResult<Trajectory> {
self.optimize_quintic(start, goal, duration)
}
pub fn optimize_with_obstacles(
&self,
start: &TrajectoryPoint,
goal: &TrajectoryPoint,
duration: f64,
obstacles: &[CircularObstacle],
) -> SpatialResult<Trajectory> {
let mut trajectory = self.optimize_quintic(start, goal, duration)?;
let safety_margin = 0.1; let mut needs_adjustment = false;
for point in trajectory.points() {
for obstacle in obstacles {
if obstacle.contains(point.x, point.y, safety_margin) {
needs_adjustment = true;
break;
}
}
if needs_adjustment {
break;
}
}
if needs_adjustment {
let midpoint_time = duration / 2.0;
let midpoint = trajectory.sample(midpoint_time)?;
let detour = self.find_detour_point(&midpoint, obstacles, safety_margin)?;
let halfway_duration = duration / 2.0;
let first_half = self.optimize_quintic(start, &detour, halfway_duration)?;
let second_half = self.optimize_quintic(&detour, goal, halfway_duration)?;
let mut combined_points = first_half.points().to_vec();
let mut second_points = second_half.points().to_vec();
for point in &mut second_points {
point.t += halfway_duration;
}
combined_points.extend(second_points);
trajectory =
Trajectory::new(combined_points, duration, OptimizationMethod::MinimumJerk);
}
Ok(trajectory)
}
fn find_detour_point(
&self,
original_point: &TrajectoryPoint,
obstacles: &[CircularObstacle],
safety_margin: f64,
) -> SpatialResult<TrajectoryPoint> {
let search_radius = 2.0;
let num_candidates = 16;
for i in 0..num_candidates {
let angle = 2.0 * PI * (i as f64) / (num_candidates as f64);
let candidate_x = original_point.x + search_radius * angle.cos();
let candidate_y = original_point.y + search_radius * angle.sin();
let mut is_valid = true;
for obstacle in obstacles {
if obstacle.contains(candidate_x, candidate_y, safety_margin) {
is_valid = false;
break;
}
}
if is_valid {
return Ok(TrajectoryPoint::with_time(
candidate_x,
candidate_y,
0.0, 0.0,
0.0, 0.0,
original_point.t,
));
}
}
Err(SpatialError::ComputationError(
"Could not find valid detour _point".to_string(),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_trajectory_point_basic() {
let point = TrajectoryPoint::new(1.0, 2.0, 0.5, -0.3, 0.1, 0.2);
assert_eq!(point.x, 1.0);
assert_eq!(point.y, 2.0);
assert_eq!(point.vx, 0.5);
assert_eq!(point.vy, -0.3);
assert_relative_eq!(
point.speed(),
(0.5_f64.powi(2) + 0.3_f64.powi(2)).sqrt(),
epsilon = 1e-10
);
}
#[test]
fn test_trajectory_constraints_default() {
let constraints = TrajectoryConstraints::default();
assert!(constraints.max_velocity > 0.0);
assert!(constraints.max_acceleration > 0.0);
assert!(constraints.time_step > 0.0);
}
#[test]
fn test_circular_obstacle() {
let obstacle = CircularObstacle::new(0.0, 0.0, 1.0);
assert!(obstacle.contains(0.5, 0.0, 0.0));
assert!(!obstacle.contains(1.5, 0.0, 0.0));
assert!(obstacle.contains(1.5, 0.0, 1.0)); }
#[test]
fn test_quintic_trajectory_optimization() {
let start = TrajectoryPoint::new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
let goal = TrajectoryPoint::new(5.0, 3.0, 0.0, 0.0, 0.0, 0.0);
let duration = 5.0;
let constraints = TrajectoryConstraints::default();
let optimizer = TrajectoryOptimizer::new(constraints);
let trajectory = optimizer
.optimize_quintic(&start, &goal, duration)
.expect("Operation failed");
let first_point = &trajectory.points()[0];
let last_point = &trajectory.points()[trajectory.len() - 1];
assert_relative_eq!(first_point.x, start.x, epsilon = 1e-6);
assert_relative_eq!(first_point.y, start.y, epsilon = 1e-6);
assert_relative_eq!(last_point.x, goal.x, epsilon = 1e-2);
assert_relative_eq!(last_point.y, goal.y, epsilon = 1e-2);
assert!(!trajectory.is_empty());
assert_eq!(trajectory.duration(), duration);
assert_eq!(trajectory.method(), OptimizationMethod::Quintic);
}
#[test]
fn test_trajectory_sampling() {
let start = TrajectoryPoint::new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
let goal = TrajectoryPoint::new(2.0, 0.0, 0.0, 0.0, 0.0, 0.0);
let duration = 2.0;
let constraints = TrajectoryConstraints::default();
let optimizer = TrajectoryOptimizer::new(constraints);
let trajectory = optimizer
.optimize_quintic(&start, &goal, duration)
.expect("Operation failed");
let start_sample = trajectory.sample(0.0).expect("Operation failed");
assert_relative_eq!(start_sample.x, start.x, epsilon = 1e-6);
assert_relative_eq!(start_sample.y, start.y, epsilon = 1e-6);
let end_sample = trajectory.sample(duration).expect("Operation failed");
assert_relative_eq!(end_sample.x, goal.x, epsilon = 1e-2);
assert_relative_eq!(end_sample.y, goal.y, epsilon = 1e-2);
assert!(trajectory.sample(-1.0).is_err());
assert!(trajectory.sample(duration + 1.0).is_err());
}
#[test]
fn test_trajectory_with_obstacles() {
let start = TrajectoryPoint::new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
let goal = TrajectoryPoint::new(4.0, 0.0, 0.0, 0.0, 0.0, 0.0);
let duration = 4.0;
let obstacles = vec![CircularObstacle::new(2.0, 1.5, 0.5)];
let constraints = TrajectoryConstraints {
max_acceleration: 50.0,
max_velocity: 50.0,
..Default::default()
};
let optimizer = TrajectoryOptimizer::new(constraints);
let trajectory_result =
optimizer.optimize_with_obstacles(&start, &goal, duration, &obstacles);
match trajectory_result {
Ok(trajectory) => {
assert!(!trajectory.is_empty());
println!(
"Successfully generated obstacle-avoiding trajectory with {} points",
trajectory.len()
);
}
Err(_) => {
println!("Obstacle avoidance algorithm couldn't find a valid trajectory - this is expected");
}
}
}
#[test]
fn test_invalid_duration() {
let start = TrajectoryPoint::new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
let goal = TrajectoryPoint::new(1.0, 0.0, 0.0, 0.0, 0.0, 0.0);
let constraints = TrajectoryConstraints::default();
let optimizer = TrajectoryOptimizer::new(constraints);
let result = optimizer.optimize_quintic(&start, &goal, -1.0);
assert!(result.is_err());
}
}