Skip to content

Commit

Permalink
Merge pull request #1139 from dhimmel/dhimmel-directional-bearings
Browse files Browse the repository at this point in the history
plot_orientation: support directed graph bearings
  • Loading branch information
gboeing authored Mar 6, 2024
2 parents ca9744b + 9cb9c99 commit bc0a9bc
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 26 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +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)
- 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)
Expand Down
58 changes: 37 additions & 21 deletions osmnx/bearing.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

from typing import overload
from warnings import warn

import networkx as nx
import numpy as np
Expand Down Expand Up @@ -122,27 +123,30 @@ def add_edge_bearings(G: nx.MultiDiGraph) -> nx.MultiDiGraph:


def orientation_entropy(
Gu: nx.MultiGraph,
G: nx.MultiGraph,
*,
num_bins: int = 36,
min_length: float = 0,
weight: str | None = None,
) -> float:
"""
Calculate undirected graph's orientation entropy.
Calculate graph's orientation entropy.
Orientation entropy is the Shannon entropy of the graphs' edges'
bidirectional bearings across evenly spaced bins. Ignores self-loop 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.
For more info see: Boeing, G. 2019. "Urban Spatial Order: Street Network
Orientation, Configuration, and Entropy." Applied Network Science, 4 (1),
67. https://doi.org/10.1007/s41109-019-0189-1
Parameters
----------
Gu
Undirected, unprojected graph with `bearing` attributes on each edge.
G
Unprojected graph with `bearing` attributes on each edge.
num_bins
Number of bins. For example, if `num_bins=36` is provided, then each
bin will represent 10 degrees around the compass.
Expand All @@ -157,32 +161,34 @@ def orientation_entropy(
Returns
-------
entropy
The orientation entropy of `Gu`.
The orientation entropy of `G`.
"""
# check if we were able to import scipy
if scipy is None: # pragma: no cover
msg = "scipy must be installed as an optional dependency to calculate entropy."
raise ImportError(msg)
bin_counts, _ = _bearings_distribution(Gu, num_bins, min_length, weight)
bin_counts, _ = _bearings_distribution(G, num_bins, min_length, weight)
entropy: float = scipy.stats.entropy(bin_counts)
return entropy


def _extract_edge_bearings(
Gu: nx.MultiGraph,
G: nx.MultiGraph,
min_length: float,
weight: str | None,
) -> npt.NDArray[np.float64]:
"""
Extract undirected graph's bidirectional edge bearings.
Extract graph's edge bearings.
For example, if an edge has a bearing of 90 degrees then we will record
A MultiGraph input receives bidirectional bearings.
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
----------
Gu
Undirected, unprojected graph with `bearing` attributes on each edge.
G
Unprojected graph with `bearing` attributes on each edge.
min_length
Ignore edges with `length` attributes less than `min_length`. Useful
to ignore the noise of many very short edges.
Expand All @@ -195,13 +201,13 @@ def _extract_edge_bearings(
Returns
-------
bearings
The bidirectional edge bearings of `Gu`.
The edge bearings of `Gu`.
"""
if nx.is_directed(Gu) or projection.is_projected(Gu.graph["crs"]): # pragma: no cover
msg = "Graph must be undirected and unprojected to analyze edge bearings."
if projection.is_projected(G.graph["crs"]): # pragma: no cover
msg = "Graph must be unprojected to analyze edge bearings."
raise ValueError(msg)
bearings = []
for u, v, data in Gu.edges(data=True):
for u, v, data in G.edges(data=True):
# ignore self-loops and any edges below min_length
if u != v and data["length"] >= min_length:
if weight:
Expand All @@ -211,15 +217,25 @@ def _extract_edge_bearings(
# don't weight bearings, just take one value per edge
bearings.append(data["bearing"])

# drop any nulls, calculate reverse bearings, concatenate and return
# drop any nulls
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)`."
)
warn(msg, category=UserWarning, stacklevel=2)
return bearings_array
# for undirected graphs, add reverse bearings and return
bearings_array_r = (bearings_array - 180) % 360
return np.concatenate([bearings_array, bearings_array_r])


def _bearings_distribution(
Gu: nx.MultiGraph,
G: nx.MultiGraph,
num_bins: int,
min_length: float,
weight: str | None,
Expand All @@ -235,8 +251,8 @@ def _bearings_distribution(
Parameters
----------
Gu
Undirected, unprojected graph with `bearing` attributes on each edge.
G
Unprojected graph with `bearing` attributes on each edge.
num_bins
Number of bins for the bearing histogram.
min_length
Expand All @@ -256,7 +272,7 @@ def _bearings_distribution(
n = num_bins * 2
bins = np.arange(n + 1) * 360 / n

bearings = _extract_edge_bearings(Gu, min_length, weight)
bearings = _extract_edge_bearings(G, min_length, weight)
count, bin_edges = np.histogram(bearings, bins=bins)

# move last bin to front, so eg 0.01 degrees and 359.99 degrees will be
Expand Down
13 changes: 8 additions & 5 deletions osmnx/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -664,7 +664,7 @@ def plot_footprints( # noqa: PLR0913


def plot_orientation( # noqa: PLR0913
Gu: nx.MultiGraph,
G: nx.MultiGraph,
*,
num_bins: int = 36,
min_length: float = 0,
Expand All @@ -682,7 +682,10 @@ def plot_orientation( # noqa: PLR0913
xtick_font: dict[str, Any] | None = None,
) -> tuple[Figure, PolarAxes]:
"""
Plot a polar histogram of a spatial network's bidirectional edge bearings.
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
`bearings` module.
Expand All @@ -693,8 +696,8 @@ def plot_orientation( # noqa: PLR0913
Parameters
----------
Gu
Undirected, unprojected graph with `bearing` attributes on each edge.
G
Unprojected graph with `bearing` attributes on each edge.
num_bins
Number of bins. For example, if `num_bins=36` is provided, then each
bin will represent 10 degrees around the compass.
Expand Down Expand Up @@ -747,7 +750,7 @@ def plot_orientation( # noqa: PLR0913

# get the bearings' distribution's bin counts and edges
bin_counts, bin_edges = bearing._bearings_distribution(
Gu,
G,
num_bins,
min_length=min_length,
weight=weight,
Expand Down
15 changes: 15 additions & 0 deletions tests/test_osmnx.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,21 @@ 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."""
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)
assert list(bearings) == [0.0] # north
bearings = ox.bearing._extract_edge_bearings(G.to_undirected(), min_length=0.0, weight=None)
assert list(bearings) == [0.0, 180.0] # north and south


def test_osm_xml() -> None:
"""Test working with .osm XML data."""
# test loading a graph from a local .osm xml file
Expand Down

0 comments on commit bc0a9bc

Please sign in to comment.