Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

plot_orientation: support directed graph bearings #1139

Merged
merged 6 commits into from
Mar 6, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
42 changes: 29 additions & 13 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,
Gu: nx.MultiGraph | nx.MultiDiGraph,
gboeing marked this conversation as resolved.
Show resolved Hide resolved
*,
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.
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 @@ -169,20 +173,22 @@ def orientation_entropy(


def _extract_edge_bearings(
Gu: nx.MultiGraph,
Gu: nx.MultiGraph | nx.MultiDiGraph,
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.
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,10 +201,10 @@ 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."
msg = "Graph must be unprojected to analyze edge bearings."
raise ValueError(msg)
bearings = []
for u, v, data in Gu.edges(data=True):
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(Gu):
# 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 `Gu.to_undirected()`."
gboeing marked this conversation as resolved.
Show resolved Hide resolved
)
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,
Gu: nx.MultiGraph | nx.MultiDiGraph,
gboeing marked this conversation as resolved.
Show resolved Hide resolved
num_bins: int,
min_length: float,
weight: str | None,
Expand All @@ -236,7 +252,7 @@ def _bearings_distribution(
Parameters
----------
Gu
Undirected, unprojected graph with `bearing` attributes on each edge.
Unprojected graph with `bearing` attributes on each edge.
num_bins
Number of bins for the bearing histogram.
min_length
Expand Down
9 changes: 6 additions & 3 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,
Gu: nx.MultiGraph | nx.MultiDiGraph,
gboeing marked this conversation as resolved.
Show resolved Hide resolved
*,
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 @@ -694,7 +697,7 @@ def plot_orientation( # noqa: PLR0913
Parameters
----------
Gu
Undirected, unprojected graph with `bearing` attributes on each edge.
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
18 changes: 18 additions & 0 deletions tests/test_osmnx.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from typeguard import TypeCheckError

import osmnx as ox
from osmnx.bearing import _extract_edge_bearings
gboeing marked this conversation as resolved.
Show resolved Hide resolved

ox.settings.log_console = True
ox.settings.log_file = True
Expand Down Expand Up @@ -141,6 +142,23 @@ def test_stats() -> None:
G_clean = ox.consolidate_intersections(G, rebuild_graph=False)


def test_extract_edge_bearings_directionality() -> None:
gboeing marked this conversation as resolved.
Show resolved Hide resolved
"""Test support of edge bearings for directed and undirected graphs."""
G = nx.MultiDiGraph(crs="epsg:4326")
lon_0, lat_0 = 0.0, 0.0
lon_1, lat_1 = 0.0, 1.0
G.add_edge(
(lon_0, lat_0),
(lon_1, lat_1),
)
G = ox.add_edge_bearings(G)
with pytest.warns(UserWarning, match="Extracting directional bearings"):
bearings = _extract_edge_bearings(G, min_length=0.0, weight=None)
assert list(bearings) == [0.0] # north
bearings = _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
Loading