From 013987de2571094b6d4f94952fe6fb5a78a4b50e Mon Sep 17 00:00:00 2001 From: huxiaoyi-ovo Date: Fri, 30 Oct 2026 00:44:51 -0400 Subject: [PATCH 1/6] fix(flowfield): restrict neighbors to traversable, goal-connected cells --- PathPlanning/FlowField/flowfield.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/PathPlanning/FlowField/flowfield.py b/PathPlanning/FlowField/flowfield.py index e50430de3c..69da6103b4 100644 --- a/PathPlanning/FlowField/flowfield.py +++ b/PathPlanning/FlowField/flowfield.py @@ -90,7 +90,7 @@ def create_integration_field(self): for i in range(-1, 2): for j in range(-1, 2): x, y = curr_x + i, curr_y + j - if self.obs_grid[(x, y)] == 'obs': + if (x, y) not in self.integration_field: continue if (i, j) in [(1, 0), (0, 1), (-1, 0), (0, -1)]: e_cost = 10 @@ -118,13 +118,19 @@ def assign_vectors(self): offset_list = [(i + a, j + b) for a in range(-1, 2) for b in range(-1, 2)] - neighbor_list = [{'loc': pt, - 'cost': self.integration_field[pt]} - for pt in offset_list - if self.obs_grid[pt] != 'obs'] - neighbor_list = sorted(neighbor_list, key=lambda x: x['cost']) - best_neighbor = neighbor_list[0]['loc'] - self.vector_field[(i, j)] = best_neighbor + # Only traversable, goal-connected cells provide directions. + neighbor_list = [ + (self.integration_field[pt], pt) + for pt in offset_list + if pt in self.integration_field + and np.isfinite(self.integration_field[pt]) + ] + if not neighbor_list: + self.vector_field[(i, j)] = (None, None) + continue + + self.vector_field[(i, j)] = min( + neighbor_list, key=lambda item: item[0])[1] def follow_vectors(self): curr_x, curr_y = self.start_pt From 8b02a8c9fd1d8dddc8d7f015c43cff8dcd58420c Mon Sep 17 00:00:00 2001 From: huxiaoyi-ovo Date: Fri, 30 Oct 2026 00:45:05 -0400 Subject: [PATCH 2/6] test(flowfield): cover unpadded boundaries and disconnected cells --- tests/test_flow_field.py | 47 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/tests/test_flow_field.py b/tests/test_flow_field.py index d049731fe5..c53fa2edc4 100644 --- a/tests/test_flow_field.py +++ b/tests/test_flow_field.py @@ -1,4 +1,5 @@ import conftest +import numpy as np import PathPlanning.FlowField.flowfield as flow_field @@ -7,5 +8,51 @@ def test(): flow_field.main() + +def test_free_grid_with_goal_at_boundary(): + flow_field.show_animation = False + grid = {(x, y): 'free' for x in range(3) for y in range(3)} + planner = flow_field.FlowField(grid, 2, 2, 0, 0, 3, 3) + planner.find_path() + + assert planner.integration_field[(2, 2)] == 0 + assert np.isfinite(planner.integration_field[(0, 0)]) + + position = (0, 0) + visited = {position} + for _ in range(9): + next_position = planner.vector_field[position] + if next_position == (None, None): + break + assert next_position not in visited + visited.add(next_position) + position = next_position + + assert position == (2, 2) + + +def test_disconnected_start_has_no_navigation_direction(): + flow_field.show_animation = False + grid = { + (x, y): ('obs' if x == 1 else 'free') + for x in range(4) for y in range(4) + } + planner = flow_field.FlowField(grid, 3, 3, 0, 0, 4, 4) + planner.find_path() + + assert np.isinf(planner.integration_field[(0, 0)]) + assert planner.vector_field[(0, 0)] == (None, None) + assert planner.integration_field[(3, 3)] == 0 + + +def test_single_cell_grid_is_valid(): + flow_field.show_animation = False + planner = flow_field.FlowField({(0, 0): 'free'}, 0, 0, 0, 0, 1, 1) + planner.find_path() + + assert planner.integration_field[(0, 0)] == 0 + assert planner.vector_field[(0, 0)] == (None, None) + + if __name__ == '__main__': conftest.run_this_test(__file__) From da66ae7d0019b67d2895b106d1a84791d6df1c20 Mon Sep 17 00:00:00 2001 From: huxiaoyi-ovo Date: Fri, 30 Oct 2026 00:45:18 -0400 Subject: [PATCH 3/6] style(tests): use standard spacing between FlowField tests --- tests/test_flow_field.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_flow_field.py b/tests/test_flow_field.py index c53fa2edc4..6e10daf1be 100644 --- a/tests/test_flow_field.py +++ b/tests/test_flow_field.py @@ -8,7 +8,6 @@ def test(): flow_field.main() - def test_free_grid_with_goal_at_boundary(): flow_field.show_animation = False grid = {(x, y): 'free' for x in range(3) for y in range(3)} From bd467af51daaa9d63931afc8397a82521d3cd917 Mon Sep 17 00:00:00 2001 From: huxiaoyi-ovo Date: Fri, 30 Oct 2026 05:00:51 -0400 Subject: [PATCH 4/6] fix(flowfield): avoid plotting termination sentinel --- PathPlanning/FlowField/flowfield.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PathPlanning/FlowField/flowfield.py b/PathPlanning/FlowField/flowfield.py index 69da6103b4..5dd5b80a2b 100644 --- a/PathPlanning/FlowField/flowfield.py +++ b/PathPlanning/FlowField/flowfield.py @@ -137,7 +137,7 @@ def follow_vectors(self): while curr_x is not None and curr_y is not None: curr_x, curr_y = self.vector_field[(curr_x, curr_y)] - if show_animation: + if show_animation and curr_x is not None and curr_y is not None: plt.plot(curr_x, curr_y, "b*") plt.pause(0.001) From 16663167e164a6b6fc6101f9452131cb35d6b445 Mon Sep 17 00:00:00 2001 From: huxiaoyi-ovo Date: Fri, 30 Oct 2026 05:01:01 -0400 Subject: [PATCH 5/6] test(flowfield): cover animated goal termination --- tests/test_flow_field.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/test_flow_field.py b/tests/test_flow_field.py index 6e10daf1be..296d5ae646 100644 --- a/tests/test_flow_field.py +++ b/tests/test_flow_field.py @@ -53,5 +53,27 @@ def test_single_cell_grid_is_valid(): assert planner.vector_field[(0, 0)] == (None, None) + + +def test_animation_skips_termination_sentinel(monkeypatch): + """Do not send (None, None) to Matplotlib after reaching the goal.""" + monkeypatch.setattr(flow_field, 'show_animation', True) + plotted = [] + shown = [] + monkeypatch.setattr( + flow_field.plt, 'plot', + lambda x, y, *args: plotted.append((x, y)), + ) + monkeypatch.setattr(flow_field.plt, 'pause', lambda *_: None) + monkeypatch.setattr(flow_field.plt, 'show', lambda: shown.append(True)) + + grid = {(0, 0): 'free', (1, 0): 'free'} + planner = flow_field.FlowField(grid, 1, 0, 0, 0, 2, 1) + planner.find_path() + + assert plotted == [(1, 0)] + assert shown == [True] + + if __name__ == '__main__': conftest.run_this_test(__file__) From c68f4a4ca891f2f0de791aea756782cf7e388183 Mon Sep 17 00:00:00 2001 From: huxiaoyi-ovo Date: Tue, 3 Nov 2026 08:17:11 -0500 Subject: [PATCH 6/6] test(flow-field): check finite descending boundary vectors Exercise every cell on an unpadded free grid and keep test formatting consistent. Co-authored-by: ChatGPT (OpenAI GPT-6) --- tests/test_flow_field.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/test_flow_field.py b/tests/test_flow_field.py index 296d5ae646..10b5613ddb 100644 --- a/tests/test_flow_field.py +++ b/tests/test_flow_field.py @@ -17,6 +17,16 @@ def test_free_grid_with_goal_at_boundary(): assert planner.integration_field[(2, 2)] == 0 assert np.isfinite(planner.integration_field[(0, 0)]) + # Every free cell has a finite cost and a valid descending direction. + for position, cost in planner.integration_field.items(): + assert np.isfinite(cost) + if position == (2, 2): + assert planner.vector_field[position] == (None, None) + continue + neighbor = planner.vector_field[position] + assert neighbor in grid + assert planner.integration_field[neighbor] < cost + position = (0, 0) visited = {position} for _ in range(9): @@ -53,8 +63,6 @@ def test_single_cell_grid_is_valid(): assert planner.vector_field[(0, 0)] == (None, None) - - def test_animation_skips_termination_sentinel(monkeypatch): """Do not send (None, None) to Matplotlib after reaching the goal.""" monkeypatch.setattr(flow_field, 'show_animation', True)