Skip to main content

rust_robotics_planning/
bezier_path_planning.rs

1#![allow(dead_code, clippy::too_many_arguments, clippy::type_complexity)]
2
3//! Bezier curve path planning
4//!
5//! Path planner using Bezier curves with curvature computation.
6
7/// Binomial coefficient calculation
8fn binomial_coefficient(n: usize, k: usize) -> f64 {
9    if k > n {
10        return 0.0;
11    }
12    if k == 0 || k == n {
13        return 1.0;
14    }
15
16    let k = if k > n - k { n - k } else { k };
17
18    let mut result = 1.0;
19    for i in 0..k {
20        result *= (n - i) as f64;
21        result /= (i + 1) as f64;
22    }
23    result
24}
25
26/// Bernstein polynomial
27fn bernstein_poly(n: usize, i: usize, t: f64) -> f64 {
28    binomial_coefficient(n, i) * t.powi(i as i32) * (1.0 - t).powi((n - i) as i32)
29}
30
31/// Return one point on the bezier curve
32fn bezier(t: f64, control_points: &[(f64, f64)]) -> (f64, f64) {
33    let n = control_points.len() - 1;
34    let mut x = 0.0;
35    let mut y = 0.0;
36
37    for (i, &(px, py)) in control_points.iter().enumerate() {
38        let basis = bernstein_poly(n, i, t);
39        x += basis * px;
40        y += basis * py;
41    }
42
43    (x, y)
44}
45
46/// Compute bezier path (trajectory) given control points
47pub fn calc_bezier_path(control_points: &[(f64, f64)], n_points: usize) -> Vec<(f64, f64)> {
48    let mut trajectory = Vec::new();
49
50    for i in 0..n_points {
51        let t = i as f64 / (n_points - 1) as f64;
52        trajectory.push(bezier(t, control_points));
53    }
54
55    trajectory
56}
57
58/// Compute control points and path given start and end position
59pub fn calc_4points_bezier_path(
60    sx: f64,
61    sy: f64,
62    syaw: f64,
63    ex: f64,
64    ey: f64,
65    eyaw: f64,
66    offset: f64,
67) -> (Vec<(f64, f64)>, Vec<(f64, f64)>) {
68    let dist = ((sx - ex).powi(2) + (sy - ey).powi(2)).sqrt() / offset;
69
70    let control_points = vec![
71        (sx, sy),
72        (sx + dist * syaw.cos(), sy + dist * syaw.sin()),
73        (ex - dist * eyaw.cos(), ey - dist * eyaw.sin()),
74        (ex, ey),
75    ];
76
77    let path = calc_bezier_path(&control_points, 100);
78
79    (path, control_points)
80}
81
82/// Compute control points of the successive derivatives of a given bezier curve
83pub fn bezier_derivatives_control_points(
84    control_points: &[(f64, f64)],
85    n_derivatives: usize,
86) -> Vec<Vec<(f64, f64)>> {
87    let mut derivatives = vec![control_points.to_vec()];
88
89    for i in 0..n_derivatives {
90        let current = &derivatives[i];
91        let n = current.len();
92
93        if n <= 1 {
94            break;
95        }
96
97        let mut next_derivative = Vec::new();
98        for j in 0..n - 1 {
99            let dx = (n - 1) as f64 * (current[j + 1].0 - current[j].0);
100            let dy = (n - 1) as f64 * (current[j + 1].1 - current[j].1);
101            next_derivative.push((dx, dy));
102        }
103        derivatives.push(next_derivative);
104    }
105
106    derivatives
107}
108
109/// Calculate curvature at parameter t
110pub fn calc_curvature(control_points: &[(f64, f64)], t: f64) -> f64 {
111    let derivatives = bezier_derivatives_control_points(control_points, 2);
112
113    if derivatives.len() < 3 {
114        return 0.0;
115    }
116
117    let first_deriv = bezier(t, &derivatives[1]);
118    let second_deriv = bezier(t, &derivatives[2]);
119
120    let dx = first_deriv.0;
121    let dy = first_deriv.1;
122    let ddx = second_deriv.0;
123    let ddy = second_deriv.1;
124
125    let numerator = dx * ddy - dy * ddx;
126    let denominator = (dx * dx + dy * dy).powf(1.5);
127
128    if denominator.abs() < 1e-10 {
129        0.0
130    } else {
131        numerator / denominator
132    }
133}
134
135pub struct BezierPathPlanner {
136    pub path: Vec<(f64, f64)>,
137    pub control_points: Vec<(f64, f64)>,
138    pub curvature: Vec<f64>,
139}
140
141impl BezierPathPlanner {
142    pub fn new() -> Self {
143        BezierPathPlanner {
144            path: Vec::new(),
145            control_points: Vec::new(),
146            curvature: Vec::new(),
147        }
148    }
149
150    pub fn planning(
151        &mut self,
152        sx: f64,
153        sy: f64,
154        syaw: f64,
155        ex: f64,
156        ey: f64,
157        eyaw: f64,
158        offset: f64,
159    ) -> bool {
160        let (path, control_points) = calc_4points_bezier_path(sx, sy, syaw, ex, ey, eyaw, offset);
161
162        let mut curvature = Vec::new();
163        for i in 0..path.len() {
164            let t = i as f64 / (path.len() - 1) as f64;
165            curvature.push(calc_curvature(&control_points, t));
166        }
167
168        self.path = path;
169        self.control_points = control_points;
170        self.curvature = curvature;
171
172        true
173    }
174
175    pub fn planning_with_control_points(
176        &mut self,
177        control_points: Vec<(f64, f64)>,
178        n_points: usize,
179    ) -> bool {
180        if control_points.len() < 2 {
181            return false;
182        }
183
184        let path = calc_bezier_path(&control_points, n_points);
185
186        let mut curvature = Vec::new();
187        for i in 0..path.len() {
188            let t = i as f64 / (path.len() - 1) as f64;
189            curvature.push(calc_curvature(&control_points, t));
190        }
191
192        self.path = path;
193        self.control_points = control_points;
194        self.curvature = curvature;
195
196        true
197    }
198}
199
200impl Default for BezierPathPlanner {
201    fn default() -> Self {
202        Self::new()
203    }
204}
205
206#[cfg(test)]
207#[allow(clippy::excessive_precision)]
208mod tests {
209    use super::*;
210
211    type PathFingerprint = (f64, f64, f64, f64);
212    type CurvatureFingerprint = (f64, f64, f64);
213
214    fn assert_point_close(actual: (f64, f64), expected: (f64, f64), tolerance: f64) {
215        assert!(
216            (actual.0 - expected.0).abs() < tolerance,
217            "x mismatch: actual={actual:?}, expected={expected:?}"
218        );
219        assert!(
220            (actual.1 - expected.1).abs() < tolerance,
221            "y mismatch: actual={actual:?}, expected={expected:?}"
222        );
223    }
224
225    fn path_fingerprint(path: &[(f64, f64)]) -> PathFingerprint {
226        let sum_x = path.iter().map(|point| point.0).sum();
227        let sum_y = path.iter().map(|point| point.1).sum();
228        let weighted_sum_x = path
229            .iter()
230            .enumerate()
231            .map(|(index, point)| (index + 1) as f64 * point.0)
232            .sum();
233        let weighted_sum_y = path
234            .iter()
235            .enumerate()
236            .map(|(index, point)| (index + 1) as f64 * point.1)
237            .sum();
238        (sum_x, sum_y, weighted_sum_x, weighted_sum_y)
239    }
240
241    fn curvature_fingerprint(curvature: &[f64]) -> CurvatureFingerprint {
242        let sum = curvature.iter().copied().sum();
243        let weighted_sum = curvature
244            .iter()
245            .enumerate()
246            .map(|(index, value)| (index + 1) as f64 * value)
247            .sum();
248        let sum_sq = curvature.iter().map(|value| value * value).sum();
249        (sum, weighted_sum, sum_sq)
250    }
251
252    fn assert_path_fingerprint_close(
253        actual: PathFingerprint,
254        expected: PathFingerprint,
255        tolerance: f64,
256    ) {
257        assert!(
258            (actual.0 - expected.0).abs() < tolerance,
259            "sum_x mismatch: actual={actual:?}, expected={expected:?}"
260        );
261        assert!(
262            (actual.1 - expected.1).abs() < tolerance,
263            "sum_y mismatch: actual={actual:?}, expected={expected:?}"
264        );
265        assert!(
266            (actual.2 - expected.2).abs() < tolerance,
267            "weighted_sum_x mismatch: actual={actual:?}, expected={expected:?}"
268        );
269        assert!(
270            (actual.3 - expected.3).abs() < tolerance,
271            "weighted_sum_y mismatch: actual={actual:?}, expected={expected:?}"
272        );
273    }
274
275    fn assert_curvature_fingerprint_close(
276        actual: CurvatureFingerprint,
277        expected: CurvatureFingerprint,
278        tolerance: f64,
279    ) {
280        assert!(
281            (actual.0 - expected.0).abs() < tolerance,
282            "sum mismatch: actual={actual:?}, expected={expected:?}"
283        );
284        assert!(
285            (actual.1 - expected.1).abs() < tolerance,
286            "weighted_sum mismatch: actual={actual:?}, expected={expected:?}"
287        );
288        assert!(
289            (actual.2 - expected.2).abs() < tolerance,
290            "sum_sq mismatch: actual={actual:?}, expected={expected:?}"
291        );
292    }
293
294    #[test]
295    fn test_bezier_path_planning() {
296        let mut planner = BezierPathPlanner::new();
297        let result = planner.planning(
298            10.0,
299            1.0,
300            180.0_f64.to_radians(),
301            0.0,
302            -3.0,
303            (-45.0_f64).to_radians(),
304            3.0,
305        );
306        assert!(result);
307        assert!(!planner.path.is_empty());
308        assert!(!planner.curvature.is_empty());
309    }
310
311    #[test]
312    fn test_bezier_with_control_points() {
313        let mut planner = BezierPathPlanner::new();
314        let cp = vec![(-1.0, 0.0), (3.0, -3.0), (4.0, 1.0), (2.0, 1.0), (1.0, 3.0)];
315        let result = planner.planning_with_control_points(cp, 100);
316        assert!(result);
317        assert!(!planner.path.is_empty());
318    }
319
320    #[test]
321    fn test_planning_matches_upstream_main_example() {
322        let mut planner = BezierPathPlanner::new();
323        planner.planning(
324            10.0,
325            1.0,
326            180.0_f64.to_radians(),
327            0.0,
328            -3.0,
329            (-45.0_f64).to_radians(),
330            3.0,
331        );
332
333        let expected_control_points = [
334            (10.0, 1.0),
335            (6.409_890_128_576_997, 1.0),
336            (-2.538_591_035_287_97, -0.461_408_964_712_031),
337            (0.0, -3.0),
338        ];
339        let expected_samples = [
340            (0usize, 10.0, 1.0),
341            (25, 6.526_392_761_056_504, 0.726_609_535_974_175),
342            (50, 2.630_199_272_944_397, -0.068_812_597_489_713),
343            (75, -0.060_978_738_129_225, -1.349_142_512_471_283),
344            (99, 0.0, -3.0),
345        ];
346
347        assert_eq!(planner.path.len(), 100);
348        for (actual, expected) in planner
349            .control_points
350            .iter()
351            .copied()
352            .zip(expected_control_points.iter().copied())
353        {
354            assert_point_close(actual, expected, 1e-12);
355        }
356        for (index, x, y) in expected_samples {
357            assert_point_close(planner.path[index], (x, y), 1e-12);
358        }
359    }
360
361    #[test]
362    fn test_calc_curvature_matches_upstream_main_example() {
363        let control_points = [
364            (10.0, 1.0),
365            (6.409_890_128_576_997, 1.0),
366            (-2.538_591_035_287_97, -0.461_408_964_712_031),
367            (0.0, -3.0),
368        ];
369
370        let curvature = calc_curvature(&control_points, 0.86);
371        assert!((curvature - 1.203_883_311_167_986).abs() < 1e-12);
372    }
373
374    #[test]
375    fn test_planning_with_control_points_matches_upstream_reference() {
376        let mut planner = BezierPathPlanner::new();
377        planner.planning_with_control_points(
378            vec![(-1.0, 0.0), (3.0, -3.0), (4.0, 1.0), (2.0, 1.0), (1.0, 3.0)],
379            100,
380        );
381
382        let expected_path_samples = [
383            (0usize, -1.0, 0.0),
384            (25, 1.908_827_926_528_655, -0.991_419_119_052_972),
385            (50, 2.749_694_941_997_521, 0.090_314_761_977_827),
386            (75, 2.108_174_996_479_529, 1.482_624_053_372_864),
387            (99, 1.0, 3.0),
388        ];
389        let expected_curvature_samples = [
390            (0usize, 0.114),
391            (25, 0.686_973_069_873_466),
392            (50, 0.778_736_996_781_883),
393            (75, 0.123_309_580_949_893),
394            (99, -0.268_328_157_299_975),
395        ];
396
397        assert_eq!(planner.path.len(), 100);
398        assert_eq!(planner.curvature.len(), 100);
399        for (index, x, y) in expected_path_samples {
400            assert_point_close(planner.path[index], (x, y), 1e-12);
401        }
402        for (index, expected) in expected_curvature_samples {
403            assert!(
404                (planner.curvature[index] - expected).abs() < 1e-12,
405                "curvature mismatch at {index}: actual={}, expected={expected}",
406                planner.curvature[index]
407            );
408        }
409        assert_path_fingerprint_close(
410            path_fingerprint(&planner.path),
411            (
412                178.183_164_845_750_3,
413                41.116_834_432_822_614,
414                10_028.516_464_168_946,
415                5_417.733_506_201_01,
416            ),
417            1e-9,
418        );
419        assert_curvature_fingerprint_close(
420            curvature_fingerprint(&planner.curvature),
421            (
422                39.037_977_305_450_994,
423                1_413.299_260_366_335_1,
424                30.142_637_421_645_173,
425            ),
426            1e-9,
427        );
428    }
429}