diff --git a/CHANGELOG.md b/CHANGELOG.md index 253986fba..fc9f006ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ Read the v2 [migration guide](https://github.com/gboeing/osmnx/issues/1123) - make which_result function parameter consistently able to accept a list throughout package (#1113) - make utils_geo.bbox_from_point function return a tuple of floats for consistency with rest of package (#1113) - change add_node_elevations_google default batch_size to 512 to match Google's limit (#1115) -- support analysis of directional edge bearings on MultiDiGraph input (#1137 #1139) +- allow analysis of MultiDiGraph directional edge bearings and orientation (#1139) - fix bug in \_downloader.\_save_to_cache function usage (#1107) - fix bug in handling requests ConnectionError when querying Overpass status endpoint (#1113) - fix minor bugs throughout to address inconsistencies revealed by type enforcement (#1107 #1114) diff --git a/osmnx/bearing.py b/osmnx/bearing.py index ab482a22a..d2ebffaf1 100644 --- a/osmnx/bearing.py +++ b/osmnx/bearing.py @@ -123,7 +123,7 @@ def add_edge_bearings(G: nx.MultiDiGraph) -> nx.MultiDiGraph: def orientation_entropy( - G: nx.MultiGraph, + G: nx.MultiGraph | nx.MultiDiGraph, *, num_bins: int = 36, min_length: float = 0, @@ -132,12 +132,12 @@ def orientation_entropy( """ Calculate graph's orientation entropy. - Orientation entropy is the Shannon entropy of the graphs' edges' - bearings across evenly spaced bins. Ignores self-loop edges - as their bearings are undefined. - - For MultiGraph input, calculates entropy of bidirectional bearings. - For MultiDiGraph input, calculates entropy of directional bearings. + Orientation entropy is the Shannon entropy of the graphs' edges' bearings + across evenly spaced bins. Ignores self-loop edges as their bearings are + undefined. If `G` is a MultiGraph, all edge bearings will be bidirectional + (ie, two reciprocal bearings per undirected edge). If `G` is a + MultiDiGraph, all edge bearings will be directional (ie, one bearing per + directed edge). For more info see: Boeing, G. 2019. "Urban Spatial Order: Street Network Orientation, Configuration, and Entropy." Applied Network Science, 4 (1), @@ -173,17 +173,19 @@ def orientation_entropy( def _extract_edge_bearings( - G: nx.MultiGraph, + G: nx.MultiGraph | nx.MultiDiGraph, min_length: float, weight: str | None, ) -> npt.NDArray[np.float64]: """ Extract graph's edge bearings. - A MultiGraph input receives bidirectional bearings. - For example, if an undirected edge has a bearing of 90 degrees then we will record + Ignores self-loop edges as their bearings are undefined. If `G` is a + MultiGraph, all edge bearings will be bidirectional (ie, two reciprocal + bearings per undirected edge). If `G` is a MultiDiGraph, all edge bearings + will be directional (ie, one bearing per directed edge). For example, if + an undirected edge has a bearing of 90 degrees then we will record bearings of both 90 degrees and 270 degrees for this edge. - For MultiDiGraph input, record only one bearing per edge. Parameters ---------- @@ -221,11 +223,10 @@ def _extract_edge_bearings( bearings_array = np.array(bearings) bearings_array = bearings_array[~np.isnan(bearings_array)] if nx.is_directed(G): - # https://github.com/gboeing/osmnx/issues/1137 msg = ( - "Extracting directional bearings (one bearing per edge) due to MultiDiGraph input. " - "To extract bidirectional bearings (two bearings per edge, including the reverse bearing), " - "supply an undirected graph instead via `osmnx.get_undirected(G)`." + "`G` is a MultiDiGraph, so edge bearings will be directional (one per " + "edge). If you want bidirectional edge bearings (two reciprocal bearings " + "per edge), pass a MultiGraph instead. Use `utils_graph.get_undirected`." ) warn(msg, category=UserWarning, stacklevel=2) return bearings_array @@ -235,7 +236,7 @@ def _extract_edge_bearings( def _bearings_distribution( - G: nx.MultiGraph, + G: nx.MultiGraph | nx.MultiDiGraph, num_bins: int, min_length: float, weight: str | None, diff --git a/osmnx/plot.py b/osmnx/plot.py index 2e2316ce8..472e2a58e 100644 --- a/osmnx/plot.py +++ b/osmnx/plot.py @@ -664,7 +664,7 @@ def plot_footprints( # noqa: PLR0913 def plot_orientation( # noqa: PLR0913 - G: nx.MultiGraph, + G: nx.MultiGraph | nx.MultiDiGraph, *, num_bins: int = 36, min_length: float = 0, @@ -684,10 +684,10 @@ def plot_orientation( # noqa: PLR0913 """ Plot a polar histogram of a spatial network's edge bearings. - A MultiGraph input receives bidirectional bearings, while a MultiDiGraph - input receives directional bearings (one bearing per edge). - - Ignores self-loop edges as their bearings are undefined. See also the + Ignores self-loop edges as their bearings are undefined. If `G` is a + MultiGraph, all edge bearings will be bidirectional (ie, two reciprocal + bearings per undirected edge). If `G` is a MultiDiGraph, all edge bearings + will be directional (ie, one bearing per directed edge). See also the `bearings` module. For more info see: Boeing, G. 2019. "Urban Spatial Order: Street Network diff --git a/tests/test_osmnx.py b/tests/test_osmnx.py index 43f292c2f..baa147c33 100644 --- a/tests/test_osmnx.py +++ b/tests/test_osmnx.py @@ -107,8 +107,6 @@ def test_stats() -> None: # create graph, add a new node, add bearings, project it G = ox.graph_from_place(place1, network_type="all") G.add_node(0, x=location_point[1], y=location_point[0], street_count=0) - _ = ox.bearing.calculate_bearing(0, 0, 1, 1) - G = ox.add_edge_bearings(G) G_proj = ox.project_graph(G) G_proj = ox.distance.add_edge_lengths(G_proj, edges=tuple(G_proj.edges)[0:3]) @@ -118,13 +116,6 @@ def test_stats() -> None: stats = ox.basic_stats(G, area=1000) stats = ox.basic_stats(G_proj, area=1000, clean_int_tol=15) - # calculate entropy - Gu = ox.get_undirected(G) - entropy = ox.bearing.orientation_entropy(Gu, weight="length") - - fig, ax = ox.plot.plot_orientation(Gu, area=True, title="Title") - fig, ax = ox.plot.plot_orientation(Gu, ax=ax, area=False, title="Title") - # test cleaning and rebuilding graph G_clean = ox.consolidate_intersections(G_proj, tolerance=10, rebuild_graph=True, dead_ends=True) G_clean = ox.consolidate_intersections( @@ -141,18 +132,35 @@ def test_stats() -> None: G_clean = ox.consolidate_intersections(G, rebuild_graph=False) -def test_extract_edge_bearings_directionality() -> None: - """Test support of edge bearings for directed and undirected graphs.""" +def test_bearings() -> None: + """Test bearings and orientation entropy.""" + G = ox.graph_from_place(place1, network_type="all") + G.add_node(0, x=location_point[1], y=location_point[0], street_count=0) + _ = ox.bearing.calculate_bearing(0, 0, 1, 1) + G = ox.add_edge_bearings(G) + G_proj = ox.project_graph(G) + + # calculate entropy + Gu = ox.get_undirected(G) + entropy = ox.bearing.orientation_entropy(Gu, weight="length") + fig, ax = ox.plot.plot_orientation(Gu, area=True, title="Title") + fig, ax = ox.plot.plot_orientation(Gu, ax=ax, area=False, title="Title") + + # test support of edge bearings for directed and undirected graphs G = nx.MultiDiGraph(crs="epsg:4326") G.add_node("point_1", x=0.0, y=0.0) G.add_node("point_2", x=0.0, y=1.0) # latitude increases northward G.add_edge("point_1", "point_2") G = ox.distance.add_edge_lengths(G) G = ox.add_edge_bearings(G) - with pytest.warns(UserWarning, match="Extracting directional bearings"): - bearings = ox.bearing._extract_edge_bearings(G, min_length=0.0, weight=None) + with pytest.warns(UserWarning, match="edge bearings will be directional"): + bearings = ox.bearing._extract_edge_bearings(G, min_length=0, weight=None) assert list(bearings) == [0.0] # north - bearings = ox.bearing._extract_edge_bearings(G.to_undirected(), min_length=0.0, weight=None) + bearings = ox.bearing._extract_edge_bearings( + ox.utils_graph.get_undirected(G), + min_length=0, + weight=None, + ) assert list(bearings) == [0.0, 180.0] # north and south