Skip to content

Commit 8398202

Browse files
authored
v2.4.1 bugfix networkx A* failure
2 parents b59a429 + d0c3774 commit 8398202

File tree

14 files changed

+422
-236
lines changed

14 files changed

+422
-236
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ repos:
5757
rev: "0.48"
5858
hooks:
5959
- id: check-manifest
60-
args: [ "--no-build-isolation", "--ignore", "*.png,docs/*,publish.py,readthedocs.yml,poetry.lock,setup.py" ]
60+
args: [ "--no-build-isolation", "--ignore", "*.png,docs/*,publish.py,readthedocs.yml,poetry.lock,setup.py,scripts/*" ]
6161
additional_dependencies: [ numpy, poetry==1.1.11 ]
6262

6363
- repo: https://github.com/asottile/pyupgrade

CHANGELOG.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,21 @@ TODOs
66
* pending major release: remove separate prepare step?! initialise in one step during initialisation
77
* Numba JIT compilation of utils. line speed profiling for highest impact of refactoring
88
* allow input of complex geometries: input coords, and edges separately (polygons are special case)
9+
* drop simplify parameter
10+
11+
2.4.1 (2022-08-22)
12+
-------------------
13+
14+
* bugfix: catch the case where no path is possible in the graph in the ``networkx`` A* implementation
15+
* added speed benchmarks and performance section in the documentation with benchmark results
16+
17+
internal:
18+
19+
* optimisation: checking edges with the biggest angle range first
20+
* optimisation: skipping visibility checks for the last extremity
21+
* using optimised point in polygon check algorithm
22+
* using undirected Graph: The precomputed graph usually makes up the majority of the visibility graph (in comparison to the temporarily added edges for query start and goal nodes) and this precomputed part has to be undirected. Use undirected graph everywhere.
23+
* added test cases
924

1025

1126
2.4.0 (2022-08-18)

docs/7_performance.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
2+
Performance
3+
===========
4+
5+
6+
.. _speed-tests:
7+
8+
Speed Benchmark Results
9+
-----------------------
10+
11+
obtained on MacBook Pro (15-inch, 2017), 2,8 GHz Intel Core i7 and `extremitypathfinder` version ``2.4.1`` using the script
12+
``scripts/speed_benchmarks.py``
13+
14+
15+
::
16+
17+
speed_benchmarks.py::test_env_preparation_speed PASSED [ 50%]
18+
avg. environment preparation time 7.4e-03 s/run, 1.4e+02 runs/s
19+
averaged over 1,000 runs
20+
21+
speed_benchmarks.py::test_query_speed PASSED [100%]
22+
avg. query time 7.1e-04 s/run, 1.4e+03 runs/s
23+
averaged over 1,000 runs

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ python package for geometric shortest path computation in 2D multi-polygon or gr
2020
Usage <1_usage>
2121
About <3_about>
2222
API <4_api>
23+
Performance <6_performance>
2324
Contributing <5_contributing>
2425
Changelog <6_changelog>
2526

extremitypathfinder/configs.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,3 @@
1-
from typing import Iterable, List, Optional, Tuple, Union
2-
3-
import numpy as np
4-
5-
COORDINATE_TYPE = Tuple[float, float]
6-
PATH_TYPE = List[COORDINATE_TYPE]
7-
LENGTH_TYPE = Optional[float]
8-
INPUT_NUMERICAL_TYPE = Union[float, int]
9-
InputCoords = Tuple[INPUT_NUMERICAL_TYPE, INPUT_NUMERICAL_TYPE]
10-
OBSTACLE_ITER_TYPE = Iterable[InputCoords]
11-
INPUT_COORD_LIST_TYPE = Union[np.ndarray, List]
121
DEFAULT_PICKLE_NAME = "environment.pickle"
132

143
# command line interface

extremitypathfinder/extremitypathfinder.py

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,9 @@
44
import networkx as nx
55
import numpy as np
66

7-
from extremitypathfinder.configs import (
8-
DEFAULT_PICKLE_NAME,
9-
INPUT_COORD_LIST_TYPE,
10-
LENGTH_TYPE,
11-
OBSTACLE_ITER_TYPE,
12-
PATH_TYPE,
13-
InputCoords,
14-
)
7+
from extremitypathfinder import types as t
8+
from extremitypathfinder.configs import DEFAULT_PICKLE_NAME
9+
from extremitypathfinder.types import InputCoord, InputCoordList, Length, ObstacleIterator, Path
1510
from extremitypathfinder.utils import (
1611
check_data_requirements,
1712
cmp_reps_n_distances,
@@ -43,9 +38,9 @@ class PolygonEnvironment:
4338
holes: List[np.ndarray]
4439
extremity_indices: List[int]
4540
reprs_n_distances: Dict[int, np.ndarray]
46-
graph: nx.DiGraph
41+
graph: t.Graph
4742
# TODO
48-
temp_graph: Optional[nx.DiGraph] = None # for storing and plotting the graph during a query
43+
temp_graph: Optional[t.Graph] = None # for storing and plotting the graph during a query
4944
boundary_polygon: np.ndarray
5045
coords: np.ndarray
5146
edge_vertex_idxs: np.ndarray
@@ -68,8 +63,8 @@ def all_vertices(self) -> List[Tuple]:
6863

6964
def store(
7065
self,
71-
boundary_coordinates: INPUT_COORD_LIST_TYPE,
72-
list_of_hole_coordinates: INPUT_COORD_LIST_TYPE,
66+
boundary_coordinates: InputCoordList,
67+
list_of_hole_coordinates: InputCoordList,
7368
validate: bool = False,
7469
):
7570
"""saves the passed input polygons in the environment
@@ -155,7 +150,7 @@ def store_grid_world(
155150
self,
156151
size_x: int,
157152
size_y: int,
158-
obstacle_iter: OBSTACLE_ITER_TYPE,
153+
obstacle_iter: ObstacleIterator,
159154
simplify: bool = True,
160155
validate: bool = False,
161156
):
@@ -247,11 +242,11 @@ def get_visible_idxs(
247242

248243
def find_shortest_path(
249244
self,
250-
start_coordinates: InputCoords,
251-
goal_coordinates: InputCoords,
245+
start_coordinates: InputCoord,
246+
goal_coordinates: InputCoord,
252247
free_space_after: bool = True,
253248
verify: bool = True,
254-
) -> Tuple[PATH_TYPE, LENGTH_TYPE]:
249+
) -> Tuple[Path, Length]:
255250
"""computes the shortest path and its length between start and goal node
256251
257252
:param start_coordinates: a (x,y) coordinate tuple representing the start node
@@ -317,7 +312,10 @@ def find_shortest_path(
317312
# graph = self.graph
318313
# nr_edges_before = len(graph.edges)
319314

320-
# add unidirectional edges in the direction: extremity (v) -> goal
315+
# add edges: extremity (i) <-> goal
316+
# Note: also here unnecessary edges in the graph could be deleted
317+
# optimising the graph here however is more expensive than beneficial,
318+
# as the graph is only being used for a single query
321319
for i in visibles_goal:
322320
graph.add_edge(i, goal, weight=vert_idx2dist[i])
323321

@@ -333,16 +331,10 @@ def find_shortest_path(
333331
# The start node does not have any neighbours. Hence there is no possible path to the goal.
334332
return [], None
335333

336-
# add edges in the direction: start -> extremity
337-
# Note: also here unnecessary edges in the graph could be deleted
338-
# optimising the graph here however is more expensive than beneficial,
339-
# as the graph is only being used for a single query
334+
# add edges: start <-> extremity (i)
340335
for i in visibles_start:
341336
graph.add_edge(start, i, weight=vert_idx2dist[i])
342337

343-
def l2_distance(n1, n2):
344-
return get_distance(n1, n2, self.reprs_n_distances)
345-
346338
# apply mapping to start and goal index as well
347339
start_mapped = find_identical_single(start, graph.nodes, self.reprs_n_distances)
348340
if start_mapped != start:
@@ -354,7 +346,13 @@ def l2_distance(n1, n2):
354346

355347
self._idx_start_tmp, self._idx_goal_tmp = start_mapped, goal_mapped # for plotting
356348

357-
id_path = nx.astar_path(graph, start_mapped, goal_mapped, heuristic=l2_distance, weight="weight")
349+
def l2_distance(n1, n2):
350+
return get_distance(n1, n2, self.reprs_n_distances)
351+
352+
try:
353+
id_path = nx.astar_path(graph, start_mapped, goal_mapped, heuristic=l2_distance, weight="weight")
354+
except nx.exception.NetworkXNoPath:
355+
return [], None
358356

359357
# clean up
360358
# TODO re-use the same graph. need to keep track of all merged edges

extremitypathfinder/plotting.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
from os.path import abspath, exists, join
44

55
import matplotlib.pyplot as plt
6-
import networkx as nx
76
from matplotlib.patches import Polygon
87

8+
from extremitypathfinder import types as t
99
from extremitypathfinder.extremitypathfinder import PolygonEnvironment
1010

1111
EXPORT_RESOLUTION = 200 # dpi
@@ -123,7 +123,7 @@ def draw_prepared_map(map):
123123
plt.show()
124124

125125

126-
def draw_with_path(map, graph: nx.DiGraph, vertex_path):
126+
def draw_with_path(map, graph: t.Graph, vertex_path):
127127
fig, ax = plt.subplots()
128128

129129
coords = map._coords_tmp
@@ -172,7 +172,7 @@ def draw_only_path(map, vertex_path, start_coordinates, goal_coordinates):
172172
plt.show()
173173

174174

175-
def draw_graph(map, graph: nx.DiGraph):
175+
def draw_graph(map, graph: t.Graph):
176176
fig, ax = plt.subplots()
177177

178178
nodes = graph.nodes

extremitypathfinder/types.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from typing import Iterable, List, Optional, Tuple, Union
2+
3+
import networkx as nx
4+
import numpy as np
5+
6+
Coordinate = Tuple[float, float]
7+
Path = List[Coordinate]
8+
Length = Optional[float]
9+
InputNumerical = Union[float, int]
10+
InputCoord = Tuple[InputNumerical, InputNumerical]
11+
InputCoordList = Union[np.ndarray, List[InputCoord]]
12+
ObstacleIterator = Iterable[InputCoord]
13+
Graph = nx.Graph

0 commit comments

Comments
 (0)