1use std::f64::consts::PI;
25
26use rust_robotics_core::error::{RoboticsError, RoboticsResult};
27use rust_robotics_core::types::{Path2D, Pose2D};
28
29pub fn fresnel(t: f64) -> (f64, f64) {
37 let t_abs = t.abs();
38
39 if t_abs < 1e-15 {
40 return (0.0, 0.0);
41 }
42
43 if t_abs > 6.0 {
45 let (c, s) = fresnel_asymptotic(t_abs);
46 if t < 0.0 {
47 return (-c, -s);
48 }
49 return (c, s);
50 }
51
52 let (c, s) = fresnel_series(t_abs);
54 if t < 0.0 {
55 (-c, -s)
56 } else {
57 (c, s)
58 }
59}
60
61fn fresnel_series(t: f64) -> (f64, f64) {
64 let half_pi = PI / 2.0;
65 let x = half_pi * t * t;
66
67 let mut c_sum = 0.0;
68 let mut s_sum = 0.0;
69 let mut term = t; let mut sign = 1.0_f64;
71
72 for k in 0..25 {
75 let c_term = sign * term / (4 * k + 1) as f64;
76 c_sum += c_term;
77
78 let s_term = sign * term * x / (4 * k + 3) as f64 / (2 * k + 1) as f64;
79 s_sum += s_term;
80
81 if c_term.abs() < 1e-16 && s_term.abs() < 1e-16 {
82 break;
83 }
84
85 term *= x * x / ((2 * k + 1) as f64 * (2 * k + 2) as f64);
87 sign = -sign;
88 }
89
90 (c_sum, s_sum)
91}
92
93fn fresnel_asymptotic(t: f64) -> (f64, f64) {
95 let x = PI * t * t;
96 let sin_x = (x / 2.0).sin();
97 let cos_x = (x / 2.0).cos();
98 let inv_x = 1.0 / (PI * t);
99
100 let pi_t2 = (PI * t).powi(2);
103 let f = inv_x * (1.0 - 3.0 / (pi_t2 * pi_t2));
104 let g = inv_x / (PI * t) * (1.0 - 15.0 / (pi_t2 * pi_t2));
105
106 let c = 0.5 + f * sin_x - g * cos_x;
107 let s = 0.5 - f * cos_x - g * sin_x;
108 (c, s)
109}
110
111pub fn clothoid_eval(theta0: f64, kappa0: f64, kappa1: f64, s: f64) -> (f64, f64, f64) {
120 let n = 100; let ds = s / n as f64;
123 let mut x = 0.0;
124 let mut y = 0.0;
125
126 for i in 0..=n {
127 let si = ds * i as f64;
128 let theta = theta0 + kappa0 * si + 0.5 * kappa1 * si * si;
129 let weight = if i == 0 || i == n {
130 1.0
131 } else if i % 2 == 1 {
132 4.0
133 } else {
134 2.0
135 };
136 x += weight * theta.cos();
137 y += weight * theta.sin();
138 }
139 x *= ds / 3.0;
140 y *= ds / 3.0;
141
142 let theta_end = theta0 + kappa0 * s + 0.5 * kappa1 * s * s;
143 (x, y, theta_end)
144}
145
146pub fn clothoid_sample(
151 x0: f64,
152 y0: f64,
153 theta0: f64,
154 kappa0: f64,
155 kappa1: f64,
156 length: f64,
157 n_points: usize,
158) -> (Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>) {
159 let mut xs = Vec::with_capacity(n_points);
160 let mut ys = Vec::with_capacity(n_points);
161 let mut thetas = Vec::with_capacity(n_points);
162 let mut kappas = Vec::with_capacity(n_points);
163
164 let cos0 = theta0.cos();
165 let sin0 = theta0.sin();
166
167 for i in 0..n_points {
168 let s = if n_points > 1 {
169 length * i as f64 / (n_points - 1) as f64
170 } else {
171 0.0
172 };
173
174 let (dx, dy, theta) = clothoid_eval(0.0, kappa0, kappa1, s);
175 let x = x0 + cos0 * dx - sin0 * dy;
177 let y = y0 + sin0 * dx + cos0 * dy;
178 let kappa = kappa0 + kappa1 * s;
179
180 xs.push(x);
181 ys.push(y);
182 thetas.push(theta0 + theta);
183 kappas.push(kappa);
184 }
185
186 (xs, ys, thetas, kappas)
187}
188
189fn normalize_angle(mut angle: f64) -> f64 {
193 while angle > PI {
194 angle -= 2.0 * PI;
195 }
196 while angle < -PI {
197 angle += 2.0 * PI;
198 }
199 angle
200}
201
202pub fn clothoid_g1_fit(start: &Pose2D, goal: &Pose2D) -> RoboticsResult<(f64, f64, f64)> {
209 let dx = goal.x - start.x;
210 let dy = goal.y - start.y;
211 let chord = (dx * dx + dy * dy).sqrt();
212
213 if chord < 1e-12 {
214 return Err(RoboticsError::InvalidParameter(
215 "start and goal poses are coincident".to_string(),
216 ));
217 }
218
219 let phi = dy.atan2(dx);
220 let alpha = normalize_angle(start.yaw - phi);
221 let beta = normalize_angle(goal.yaw - phi);
222
223 let mut length = chord;
225
226 let max_iter = 50;
231 let tol = 1e-12;
232
233 let mut k0 = (alpha + beta) / length;
234 let mut k1 = 2.0 * (beta - alpha) / (length * length);
235
236 for _iter in 0..max_iter {
237 let (ex, ey, _etheta) = clothoid_eval(alpha, k0, k1, length);
239
240 let rx = ex - chord;
242 let ry = ey;
243 let r_theta = alpha + k0 * length + 0.5 * k1 * length * length - beta;
244
245 if rx.abs() < tol && ry.abs() < tol && r_theta.abs() < tol {
246 break;
247 }
248
249 let eps = 1e-8;
251
252 let (ex_dk0, ey_dk0, _) = clothoid_eval(alpha, k0 + eps, k1, length);
253 let dex_dk0 = (ex_dk0 - ex) / eps;
254 let dey_dk0 = (ey_dk0 - ey) / eps;
255
256 let (ex_dk1, ey_dk1, _) = clothoid_eval(alpha, k0, k1 + eps, length);
257 let dex_dk1 = (ex_dk1 - ex) / eps;
258 let dey_dk1 = (ey_dk1 - ey) / eps;
259
260 let (ex_dl, ey_dl, _) = clothoid_eval(alpha, k0, k1, length + eps);
261 let dex_dl = (ex_dl - ex) / eps;
262 let dey_dl = (ey_dl - ey) / eps;
263
264 let dtheta_dk0 = length;
266 let dtheta_dk1 = 0.5 * length * length;
267 let dtheta_dl = k0 + k1 * length;
268
269 let jac = nalgebra::Matrix3::new(
271 dex_dk0, dex_dk1, dex_dl, dey_dk0, dey_dk1, dey_dl, dtheta_dk0, dtheta_dk1, dtheta_dl,
272 );
273
274 let residual = nalgebra::Vector3::new(rx, ry, r_theta);
275
276 let Some(delta) = jac.try_inverse().map(|inv| inv * residual) else {
277 return Err(RoboticsError::NumericalError(
278 "Jacobian is singular during clothoid fitting".to_string(),
279 ));
280 };
281
282 k0 -= delta[0];
283 k1 -= delta[1];
284 length -= delta[2];
285
286 if length < 1e-10 {
288 length = chord * 0.5;
289 }
290 }
291
292 let (ex, ey, _) = clothoid_eval(alpha, k0, k1, length);
294 let err = ((ex - chord).powi(2) + ey.powi(2)).sqrt();
295 if err > 1e-6 {
296 return Err(RoboticsError::NumericalError(format!(
297 "clothoid G1 fitting did not converge (residual={err:.6e})"
298 )));
299 }
300
301 Ok((k0, k1, length))
302}
303
304#[derive(Debug, Clone)]
308pub struct ClothoidPath {
309 pub start: Pose2D,
311 pub goal: Pose2D,
313 pub kappa0: f64,
315 pub kappa1: f64,
317 pub length: f64,
319 pub path: Path2D,
321 pub yaw: Vec<f64>,
323 pub curvature: Vec<f64>,
325}
326
327#[derive(Debug, Clone)]
329pub struct ClothoidConfig {
330 pub n_points: usize,
332}
333
334impl Default for ClothoidConfig {
335 fn default() -> Self {
336 Self { n_points: 100 }
337 }
338}
339
340#[derive(Debug, Clone)]
345pub struct ClothoidPlanner {
346 pub config: ClothoidConfig,
347}
348
349impl ClothoidPlanner {
350 pub fn new(config: ClothoidConfig) -> Self {
351 Self { config }
352 }
353
354 pub fn plan(&self, start: &Pose2D, goal: &Pose2D) -> RoboticsResult<ClothoidPath> {
356 let (kappa0, kappa1, length) = clothoid_g1_fit(start, goal)?;
357
358 let (xs, ys, yaw, curvature) = clothoid_sample(
359 start.x,
360 start.y,
361 start.yaw,
362 kappa0,
363 kappa1,
364 length,
365 self.config.n_points,
366 );
367
368 let path = Path2D::from_xy(&xs, &ys);
369
370 Ok(ClothoidPath {
371 start: *start,
372 goal: *goal,
373 kappa0,
374 kappa1,
375 length,
376 path,
377 yaw,
378 curvature,
379 })
380 }
381
382 pub fn plan_from_xy(
384 &self,
385 x0: f64,
386 y0: f64,
387 yaw0: f64,
388 x1: f64,
389 y1: f64,
390 yaw1: f64,
391 ) -> RoboticsResult<ClothoidPath> {
392 self.plan(&Pose2D::new(x0, y0, yaw0), &Pose2D::new(x1, y1, yaw1))
393 }
394}
395
396#[cfg(test)]
399mod tests {
400 use super::*;
401 use std::f64::consts::FRAC_PI_2;
402
403 const TOL: f64 = 1e-6;
404
405 fn assert_close(actual: f64, expected: f64, tol: f64, label: &str) {
406 assert!(
407 (actual - expected).abs() < tol,
408 "{label}: actual={actual}, expected={expected}, diff={}",
409 (actual - expected).abs()
410 );
411 }
412
413 #[test]
416 fn test_fresnel_zero() {
417 let (c, s) = fresnel(0.0);
418 assert_close(c, 0.0, 1e-15, "C(0)");
419 assert_close(s, 0.0, 1e-15, "S(0)");
420 }
421
422 #[test]
423 fn test_fresnel_known_values() {
424 let (c, s) = fresnel(1.0);
426 assert_close(c, 0.7798934003768228, 1e-10, "C(1)");
427 assert_close(s, 0.4382591473903548, 1e-10, "S(1)");
428 }
429
430 #[test]
431 fn test_fresnel_odd_symmetry() {
432 let (c_pos, s_pos) = fresnel(2.0);
433 let (c_neg, s_neg) = fresnel(-2.0);
434 assert_close(c_neg, -c_pos, 1e-14, "C(-t) = -C(t)");
435 assert_close(s_neg, -s_pos, 1e-14, "S(-t) = -S(t)");
436 }
437
438 #[test]
439 fn test_fresnel_large_argument_limit() {
440 let (c, s) = fresnel(100.0);
442 assert_close(c, 0.5, 0.005, "C(inf)");
443 assert_close(s, 0.5, 0.005, "S(inf)");
444 }
445
446 #[test]
449 fn test_clothoid_eval_straight_line() {
450 let (x, y, theta) = clothoid_eval(0.0, 0.0, 0.0, 5.0);
452 assert_close(x, 5.0, TOL, "x straight");
453 assert_close(y, 0.0, TOL, "y straight");
454 assert_close(theta, 0.0, TOL, "theta straight");
455 }
456
457 #[test]
458 fn test_clothoid_eval_circular_arc() {
459 let radius = 10.0;
461 let kappa0 = 1.0 / radius;
462 let arc_length = FRAC_PI_2 * radius; let (x, y, theta) = clothoid_eval(0.0, kappa0, 0.0, arc_length);
465 assert_close(x, radius, 0.01, "x circular arc");
467 assert_close(y, radius, 0.01, "y circular arc");
468 assert_close(theta, FRAC_PI_2, TOL, "theta circular arc");
469 }
470
471 #[test]
472 fn test_clothoid_eval_with_sharpness() {
473 let (x, y, theta) = clothoid_eval(0.0, 0.0, 0.1, 3.0);
475 assert_close(theta, 0.45, TOL, "theta with sharpness");
477 assert!(x.is_finite(), "x should be finite");
478 assert!(y.is_finite(), "y should be finite");
479 assert!(y > 0.0, "curve should bend left (positive y)");
480 }
481
482 #[test]
485 fn test_clothoid_sample_straight() {
486 let (xs, ys, thetas, kappas) = clothoid_sample(0.0, 0.0, 0.0, 0.0, 0.0, 10.0, 11);
487
488 assert_eq!(xs.len(), 11);
489 for i in 0..11 {
490 let s = i as f64;
491 assert_close(xs[i], s, TOL, &format!("x[{i}]"));
492 assert_close(ys[i], 0.0, TOL, &format!("y[{i}]"));
493 assert_close(thetas[i], 0.0, TOL, &format!("theta[{i}]"));
494 assert_close(kappas[i], 0.0, TOL, &format!("kappa[{i}]"));
495 }
496 }
497
498 #[test]
499 fn test_clothoid_sample_rotated_straight() {
500 let angle = PI / 4.0;
502 let length = 10.0;
503 let (xs, ys, _thetas, _kappas) = clothoid_sample(1.0, 2.0, angle, 0.0, 0.0, length, 11);
504
505 let end_x = 1.0 + length * angle.cos();
506 let end_y = 2.0 + length * angle.sin();
507 assert_close(*xs.last().unwrap(), end_x, TOL, "end x");
508 assert_close(*ys.last().unwrap(), end_y, TOL, "end y");
509 }
510
511 #[test]
514 fn test_g1_fit_straight_line() {
515 let start = Pose2D::new(0.0, 0.0, 0.0);
516 let goal = Pose2D::new(10.0, 0.0, 0.0);
517
518 let (kappa0, kappa1, length) = clothoid_g1_fit(&start, &goal).unwrap();
519 assert_close(kappa0, 0.0, 1e-4, "kappa0 straight");
520 assert_close(kappa1, 0.0, 1e-4, "kappa1 straight");
521 assert_close(length, 10.0, 0.1, "length straight");
522 }
523
524 #[test]
525 fn test_g1_fit_simple_curve() {
526 let start = Pose2D::new(0.0, 0.0, 0.0);
527 let goal = Pose2D::new(5.0, 3.0, PI / 4.0);
528
529 let result = clothoid_g1_fit(&start, &goal);
530 assert!(result.is_ok(), "G1 fit should converge");
531
532 let (kappa0, kappa1, length) = result.unwrap();
533 assert!(length > 0.0, "length should be positive");
534
535 let (xs, ys, thetas, _) =
537 clothoid_sample(start.x, start.y, start.yaw, kappa0, kappa1, length, 100);
538
539 let end_x = *xs.last().unwrap();
540 let end_y = *ys.last().unwrap();
541 let end_theta = *thetas.last().unwrap();
542
543 assert_close(end_x, goal.x, 0.01, "fitted end x");
544 assert_close(end_y, goal.y, 0.01, "fitted end y");
545 assert_close(
546 normalize_angle(end_theta),
547 normalize_angle(goal.yaw),
548 0.01,
549 "fitted end yaw",
550 );
551 }
552
553 #[test]
554 fn test_g1_fit_coincident_points_error() {
555 let start = Pose2D::new(1.0, 2.0, 0.0);
556 let goal = Pose2D::new(1.0, 2.0, PI / 2.0);
557
558 let result = clothoid_g1_fit(&start, &goal);
559 assert!(result.is_err(), "coincident points should fail");
560 }
561
562 #[test]
563 fn test_g1_fit_symmetry() {
564 let start = Pose2D::new(0.0, 0.0, 0.0);
567 let goal = Pose2D::new(10.0, 0.0, 0.0);
568
569 let (kappa0, kappa1, length) = clothoid_g1_fit(&start, &goal).unwrap();
570
571 assert_close(kappa0, 0.0, 1e-4, "symmetric kappa0");
572 assert_close(kappa1, 0.0, 1e-4, "symmetric kappa1");
573 assert!(length > 0.0, "length positive");
574 }
575
576 #[test]
579 fn test_planner_basic() {
580 let planner = ClothoidPlanner::new(ClothoidConfig { n_points: 50 });
581 let start = Pose2D::new(0.0, 0.0, 0.0);
582 let goal = Pose2D::new(8.0, 4.0, PI / 3.0);
583
584 let result = planner.plan(&start, &goal);
585 assert!(result.is_ok(), "planner should succeed");
586
587 let clothoid = result.unwrap();
588 assert_eq!(clothoid.path.len(), 50);
589 assert_eq!(clothoid.yaw.len(), 50);
590 assert_eq!(clothoid.curvature.len(), 50);
591
592 assert_close(clothoid.path.points[0].x, start.x, TOL, "start x");
594 assert_close(clothoid.path.points[0].y, start.y, TOL, "start y");
595
596 let first_k = clothoid.curvature[0];
598 let last_k = *clothoid.curvature.last().unwrap();
599 let mid_k = clothoid.curvature[25];
600 let expected_mid = (first_k + last_k) / 2.0;
601 assert_close(mid_k, expected_mid, 0.01, "linear curvature at midpoint");
602 }
603
604 #[test]
605 fn test_planner_from_xy() {
606 let planner = ClothoidPlanner::new(ClothoidConfig::default());
607 let result = planner.plan_from_xy(0.0, 0.0, 0.0, 5.0, 0.0, 0.0);
608 assert!(result.is_ok());
609 assert_eq!(result.unwrap().path.len(), 100);
610 }
611
612 #[test]
613 fn test_curvature_linearity() {
614 let planner = ClothoidPlanner::new(ClothoidConfig { n_points: 101 });
615 let start = Pose2D::new(0.0, 0.0, 0.0);
616 let goal = Pose2D::new(6.0, 2.0, 0.5);
617
618 let clothoid = planner.plan(&start, &goal).unwrap();
619
620 let n = clothoid.curvature.len();
623 let step = (clothoid.curvature[n - 1] - clothoid.curvature[0]) / (n - 1) as f64;
624 for i in 0..n {
625 let expected = clothoid.curvature[0] + step * i as f64;
626 assert_close(
627 clothoid.curvature[i],
628 expected,
629 1e-10,
630 &format!("curvature linearity at {i}"),
631 );
632 }
633 }
634}