1use std::cmp::Reverse;
11use std::collections::BinaryHeap;
12
13use crate::grid::GridMap;
14use rust_robotics_core::{Obstacles, Path2D, PathPlanner, Point2D, RoboticsError, RoboticsResult};
15
16#[derive(Debug, Clone)]
18pub struct FlowFieldConfig {
19 pub resolution: f64,
21 pub robot_radius: f64,
23}
24
25impl Default for FlowFieldConfig {
26 fn default() -> Self {
27 Self {
28 resolution: 1.0,
29 robot_radius: 0.5,
30 }
31 }
32}
33
34impl FlowFieldConfig {
35 pub fn validate(&self) -> RoboticsResult<()> {
36 if !self.resolution.is_finite() || self.resolution <= 0.0 {
37 return Err(RoboticsError::InvalidParameter(format!(
38 "resolution must be positive and finite, got {}",
39 self.resolution
40 )));
41 }
42 if !self.robot_radius.is_finite() || self.robot_radius < 0.0 {
43 return Err(RoboticsError::InvalidParameter(format!(
44 "robot_radius must be non-negative and finite, got {}",
45 self.robot_radius
46 )));
47 }
48 Ok(())
49 }
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54struct FlowDir {
55 dx: i32,
56 dy: i32,
57}
58
59#[derive(Debug, Clone)]
65pub struct FlowFieldResult {
66 pub integration: Vec<u32>,
69 directions: Vec<FlowDir>,
71 pub x_width: i32,
73 pub y_width: i32,
75}
76
77pub struct FlowFieldPlanner {
79 grid_map: GridMap,
80 #[allow(dead_code)]
81 config: FlowFieldConfig,
82 motion: [(i32, i32, u32); 8],
84}
85
86impl FlowFieldPlanner {
87 pub fn new(ox: &[f64], oy: &[f64], config: FlowFieldConfig) -> Self {
89 Self::try_new(ox, oy, config).expect(
90 "invalid flow field planner input: obstacle list must be non-empty and valid, \
91 and config values must be positive/finite",
92 )
93 }
94
95 pub fn try_new(ox: &[f64], oy: &[f64], config: FlowFieldConfig) -> RoboticsResult<Self> {
97 config.validate()?;
98 let grid_map = GridMap::try_new(ox, oy, config.resolution, config.robot_radius)?;
99 Ok(Self {
100 grid_map,
101 config,
102 motion: Self::motion_model(),
103 })
104 }
105
106 pub fn from_obstacles(ox: &[f64], oy: &[f64], resolution: f64, robot_radius: f64) -> Self {
108 let config = FlowFieldConfig {
109 resolution,
110 robot_radius,
111 };
112 Self::new(ox, oy, config)
113 }
114
115 pub fn from_obstacle_points(
117 obstacles: &Obstacles,
118 config: FlowFieldConfig,
119 ) -> RoboticsResult<Self> {
120 config.validate()?;
121 let grid_map = GridMap::from_obstacles(obstacles, config.resolution, config.robot_radius)?;
122 Ok(Self {
123 grid_map,
124 config,
125 motion: Self::motion_model(),
126 })
127 }
128
129 pub fn grid_map(&self) -> &GridMap {
131 &self.grid_map
132 }
133
134 pub fn plan(&self, start: Point2D, goal: Point2D) -> RoboticsResult<Path2D> {
137 self.plan_impl(start, goal)
138 }
139
140 pub fn plan_xy(&self, sx: f64, sy: f64, gx: f64, gy: f64) -> RoboticsResult<Path2D> {
142 self.plan_impl(Point2D::new(sx, sy), Point2D::new(gx, gy))
143 }
144
145 #[deprecated(note = "use plan() or plan_xy() instead")]
147 pub fn planning(&self, sx: f64, sy: f64, gx: f64, gy: f64) -> Option<(Vec<f64>, Vec<f64>)> {
148 match self.plan_xy(sx, sy, gx, gy) {
149 Ok(path) => Some((path.x_coords(), path.y_coords())),
150 Err(_) => None,
151 }
152 }
153
154 pub fn compute_field(&self, goal: Point2D) -> RoboticsResult<FlowFieldResult> {
157 let gx = self.grid_map.calc_x_index(goal.x);
158 let gy = self.grid_map.calc_y_index(goal.y);
159 self.ensure_valid(gx, gy, "Goal")?;
160 Ok(self.build_field(gx, gy))
161 }
162
163 fn motion_model() -> [(i32, i32, u32); 8] {
168 [
169 (1, 0, 10),
170 (0, 1, 10),
171 (-1, 0, 10),
172 (0, -1, 10),
173 (-1, -1, 14),
174 (-1, 1, 14),
175 (1, -1, 14),
176 (1, 1, 14),
177 ]
178 }
179
180 fn ensure_valid(&self, x: i32, y: i32, label: &str) -> RoboticsResult<()> {
181 if self.grid_map.is_valid(x, y) {
182 Ok(())
183 } else {
184 Err(RoboticsError::PlanningError(format!(
185 "{} position is invalid",
186 label
187 )))
188 }
189 }
190
191 fn flat_index(&self, x: i32, y: i32) -> usize {
192 (x as usize) * (self.grid_map.y_width as usize) + (y as usize)
193 }
194
195 fn build_field(&self, gx: i32, gy: i32) -> FlowFieldResult {
198 let w = self.grid_map.x_width as usize;
199 let h = self.grid_map.y_width as usize;
200 let total = w * h;
201
202 let mut integration = vec![u32::MAX; total];
203 let mut directions = vec![FlowDir { dx: 0, dy: 0 }; total];
204
205 let goal_idx = self.flat_index(gx, gy);
207 integration[goal_idx] = 0;
208
209 let mut open = BinaryHeap::new();
210 open.push((Reverse(0_u32), gx, gy));
211
212 while let Some((Reverse(curr_cost), cx, cy)) = open.pop() {
213 if curr_cost != integration[self.flat_index(cx, cy)] {
214 continue;
215 }
216
217 let curr_cost = integration[self.flat_index(cx, cy)];
218 for &(dx, dy, move_cost) in &self.motion {
219 let nx = cx + dx;
220 let ny = cy + dy;
221 if !self.grid_map.is_valid_offset(cx, cy, dx, dy) {
222 continue;
223 }
224 let ni = self.flat_index(nx, ny);
225 let new_cost = curr_cost.saturating_add(move_cost);
226 if new_cost < integration[ni] {
227 integration[ni] = new_cost;
228 open.push((Reverse(new_cost), nx, ny));
229 }
230 }
231 }
232
233 for ix in 0..self.grid_map.x_width {
236 for iy in 0..self.grid_map.y_width {
237 if !self.grid_map.is_valid(ix, iy) {
238 continue;
239 }
240 let idx = self.flat_index(ix, iy);
241 if integration[idx] == 0 {
242 continue;
244 }
245 if integration[idx] == u32::MAX {
246 continue;
248 }
249
250 let current_path_cost = integration[idx];
251 let mut best_neighbor_cost = u32::MAX;
252 let mut best_dx = 0i32;
253 let mut best_dy = 0i32;
254 for &(dx, dy, move_cost) in &self.motion {
255 let nx = ix + dx;
256 let ny = iy + dy;
257 if nx < 0
258 || ny < 0
259 || nx >= self.grid_map.x_width
260 || ny >= self.grid_map.y_width
261 || !self.grid_map.is_valid_offset(ix, iy, dx, dy)
262 {
263 continue;
264 }
265 let ni = self.flat_index(nx, ny);
266 if integration[ni] == u32::MAX {
267 continue;
268 }
269 let candidate_path_cost = integration[ni].saturating_add(move_cost);
270 if candidate_path_cost == current_path_cost
271 && integration[ni] < best_neighbor_cost
272 {
273 best_neighbor_cost = integration[ni];
274 best_dx = dx;
275 best_dy = dy;
276 }
277 }
278 directions[idx] = FlowDir {
279 dx: best_dx,
280 dy: best_dy,
281 };
282 }
283 }
284
285 FlowFieldResult {
286 integration,
287 directions,
288 x_width: self.grid_map.x_width,
289 y_width: self.grid_map.y_width,
290 }
291 }
292
293 fn extract_path(
295 &self,
296 field: &FlowFieldResult,
297 sx: i32,
298 sy: i32,
299 gx: i32,
300 gy: i32,
301 ) -> RoboticsResult<Path2D> {
302 let mut points = Vec::new();
303 let mut cx = sx;
304 let mut cy = sy;
305
306 let max_steps = (self.grid_map.x_width as usize) * (self.grid_map.y_width as usize);
308
309 for _ in 0..max_steps {
310 points.push(Point2D::new(
311 self.grid_map.calc_x_position(cx),
312 self.grid_map.calc_y_position(cy),
313 ));
314
315 if cx == gx && cy == gy {
316 return Ok(Path2D::from_points(points));
317 }
318
319 let idx = self.flat_index(cx, cy);
320 let dir = field.directions[idx];
321 if dir.dx == 0 && dir.dy == 0 {
322 return Err(RoboticsError::PlanningError(
324 "No path found: flow field has no direction at current cell".to_string(),
325 ));
326 }
327 cx += dir.dx;
328 cy += dir.dy;
329 }
330
331 Err(RoboticsError::PlanningError(
332 "No path found: exceeded maximum step count".to_string(),
333 ))
334 }
335
336 fn plan_impl(&self, start: Point2D, goal: Point2D) -> RoboticsResult<Path2D> {
337 let sx = self.grid_map.calc_x_index(start.x);
338 let sy = self.grid_map.calc_y_index(start.y);
339 let gx = self.grid_map.calc_x_index(goal.x);
340 let gy = self.grid_map.calc_y_index(goal.y);
341
342 self.ensure_valid(sx, sy, "Start")?;
343 self.ensure_valid(gx, gy, "Goal")?;
344
345 let field = self.build_field(gx, gy);
346 self.extract_path(&field, sx, sy, gx, gy)
347 }
348}
349
350impl PathPlanner for FlowFieldPlanner {
351 fn plan(&self, start: Point2D, goal: Point2D) -> Result<Path2D, RoboticsError> {
352 self.plan_impl(start, goal)
353 }
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359
360 use crate::moving_ai::{MovingAiMap, MovingAiScenario};
361 use rust_robotics_core::Obstacles;
362
363 fn create_simple_obstacles() -> (Vec<f64>, Vec<f64>) {
365 let mut ox = Vec::new();
366 let mut oy = Vec::new();
367
368 for i in 0..11 {
369 ox.push(i as f64);
370 oy.push(0.0);
371 ox.push(i as f64);
372 oy.push(10.0);
373 ox.push(0.0);
374 oy.push(i as f64);
375 ox.push(10.0);
376 oy.push(i as f64);
377 }
378
379 for i in 4..7 {
381 ox.push(5.0);
382 oy.push(i as f64);
383 }
384
385 (ox, oy)
386 }
387
388 #[test]
389 fn test_flow_field_finds_path() {
390 let (ox, oy) = create_simple_obstacles();
391 let planner = FlowFieldPlanner::from_obstacles(&ox, &oy, 1.0, 0.5);
392
393 let result = planner.plan(Point2D::new(2.0, 2.0), Point2D::new(8.0, 8.0));
394 assert!(result.is_ok(), "expected path, got {:?}", result);
395
396 let path = result.unwrap();
397 assert!(!path.is_empty());
398 let first = path.points.first().unwrap();
400 let last = path.points.last().unwrap();
401 assert!((first.x - 2.0).abs() < 1e-6);
402 assert!((first.y - 2.0).abs() < 1e-6);
403 assert!((last.x - 8.0).abs() < 1e-6);
404 assert!((last.y - 8.0).abs() < 1e-6);
405 }
406
407 #[test]
408 #[allow(deprecated)]
409 fn test_flow_field_legacy_interface() {
410 let (ox, oy) = create_simple_obstacles();
411 let planner = FlowFieldPlanner::from_obstacles(&ox, &oy, 1.0, 0.5);
412
413 let result = planner.planning(2.0, 2.0, 8.0, 8.0);
414 assert!(result.is_some());
415
416 let (rx, ry) = result.unwrap();
417 assert!(!rx.is_empty());
418 assert_eq!(rx.len(), ry.len());
419 }
420
421 #[test]
422 fn test_flow_field_from_obstacle_points() {
423 let (ox, oy) = create_simple_obstacles();
424 let obstacles = Obstacles::try_from_xy(&ox, &oy).unwrap();
425 let planner =
426 FlowFieldPlanner::from_obstacle_points(&obstacles, FlowFieldConfig::default()).unwrap();
427
428 let path = planner.plan_xy(2.0, 2.0, 8.0, 8.0).unwrap();
429 assert!(!path.is_empty());
430 }
431
432 #[test]
433 fn test_flow_field_invalid_config() {
434 let (ox, oy) = create_simple_obstacles();
435 let config = FlowFieldConfig {
436 resolution: -1.0,
437 ..Default::default()
438 };
439 let err = FlowFieldPlanner::try_new(&ox, &oy, config);
440 assert!(err.is_err());
441 }
442
443 #[test]
444 fn test_flow_field_invalid_start() {
445 let (ox, oy) = create_simple_obstacles();
446 let planner = FlowFieldPlanner::from_obstacles(&ox, &oy, 1.0, 0.5);
447
448 let result = planner.plan(Point2D::new(-5.0, -5.0), Point2D::new(8.0, 8.0));
449 assert!(result.is_err());
450 }
451
452 #[test]
453 fn test_flow_field_invalid_goal() {
454 let (ox, oy) = create_simple_obstacles();
455 let planner = FlowFieldPlanner::from_obstacles(&ox, &oy, 1.0, 0.5);
456
457 let result = planner.plan(Point2D::new(2.0, 2.0), Point2D::new(-5.0, -5.0));
458 assert!(result.is_err());
459 }
460
461 #[test]
462 fn test_flow_field_start_equals_goal() {
463 let (ox, oy) = create_simple_obstacles();
464 let planner = FlowFieldPlanner::from_obstacles(&ox, &oy, 1.0, 0.5);
465
466 let path = planner
467 .plan(Point2D::new(3.0, 3.0), Point2D::new(3.0, 3.0))
468 .unwrap();
469 assert_eq!(path.points.len(), 1);
471 }
472
473 #[test]
474 fn test_flow_field_preserves_asymmetric_coordinates() {
475 let mut obstacles = Obstacles::new();
476
477 for x in 10..=20 {
478 obstacles.push(Point2D::new(x as f64, -4.0));
479 obstacles.push(Point2D::new(x as f64, 6.0));
480 }
481 for y in -4..=6 {
482 obstacles.push(Point2D::new(10.0, y as f64));
483 obstacles.push(Point2D::new(20.0, y as f64));
484 }
485
486 let planner =
487 FlowFieldPlanner::from_obstacle_points(&obstacles, FlowFieldConfig::default()).unwrap();
488 let path = planner.plan_xy(12.0, -2.0, 18.0, 4.0).unwrap();
489
490 let first = path.points.first().unwrap();
491 let last = path.points.last().unwrap();
492 assert!((first.x - 12.0).abs() < 1e-6);
493 assert!((first.y + 2.0).abs() < 1e-6);
494 assert!((last.x - 18.0).abs() < 1e-6);
495 assert!((last.y - 4.0).abs() < 1e-6);
496 }
497
498 #[test]
499 fn test_compute_field_returns_valid_integration() {
500 let (ox, oy) = create_simple_obstacles();
501 let planner = FlowFieldPlanner::from_obstacles(&ox, &oy, 1.0, 0.5);
502
503 let field = planner.compute_field(Point2D::new(8.0, 8.0)).unwrap();
504
505 let gx = planner.grid_map().calc_x_index(8.0);
507 let gy = planner.grid_map().calc_y_index(8.0);
508 let goal_idx = (gx as usize) * (planner.grid_map().y_width as usize) + (gy as usize);
509 assert_eq!(field.integration[goal_idx], 0);
510 }
511
512 #[test]
513 fn test_flow_field_path_monotonically_decreases_cost() {
514 let (ox, oy) = create_simple_obstacles();
515 let planner = FlowFieldPlanner::from_obstacles(&ox, &oy, 1.0, 0.5);
516
517 let goal = Point2D::new(8.0, 8.0);
518 let field = planner.compute_field(goal).unwrap();
519
520 let path = planner.plan(Point2D::new(2.0, 2.0), goal).unwrap();
521
522 let costs: Vec<u32> = path
524 .points
525 .iter()
526 .map(|p| {
527 let ix = planner.grid_map().calc_x_index(p.x);
528 let iy = planner.grid_map().calc_y_index(p.y);
529 let idx = (ix as usize) * (planner.grid_map().y_width as usize) + (iy as usize);
530 field.integration[idx]
531 })
532 .collect();
533
534 for window in costs.windows(2) {
535 assert!(
536 window[0] >= window[1],
537 "cost should decrease along path, got {} -> {}",
538 window[0],
539 window[1]
540 );
541 }
542 }
543
544 #[test]
545 #[ignore = "long-running MovingAI benchmark"]
546 fn test_flow_field_matches_moving_ai_random512_bucket_80_optimal_length() {
547 let map = MovingAiMap::parse_str(include_str!(
548 "../benchdata/moving_ai/random/random512-10-0.map"
549 ))
550 .expect("random512 map should parse");
551 let scenario = MovingAiScenario::parse_str(include_str!(
552 "../benchdata/moving_ai/random/random512-10-0.map.scen"
553 ))
554 .expect("random512 scenario should parse")
555 .into_iter()
556 .find(|entry| entry.bucket == 80)
557 .expect("random512 MovingAI bucket 80 scenario should exist");
558
559 let obstacles = map.to_obstacles();
560 let planner = FlowFieldPlanner::from_obstacle_points(
561 &obstacles,
562 FlowFieldConfig {
563 resolution: 1.0,
564 robot_radius: 0.0,
565 },
566 )
567 .expect("flow field planner should build from random512 obstacles");
568
569 let start = map
570 .planning_point(scenario.start_x, scenario.start_y)
571 .expect("random512 start should map to planner coordinates");
572 let goal = map
573 .planning_point(scenario.goal_x, scenario.goal_y)
574 .expect("random512 goal should map to planner coordinates");
575
576 let path = planner
577 .plan(start, goal)
578 .expect("FlowField should solve the random512 bucket 80 scenario");
579
580 assert!(
581 (path.total_length() - scenario.optimal_length).abs() < 1e-6,
582 "FlowField path length {} should match MovingAI optimal {}",
583 path.total_length(),
584 scenario.optimal_length
585 );
586 }
587}