Skip to content

Commit

Permalink
ENH: always store x, y as node attributes in gdf_to_nx, optionally ca…
Browse files Browse the repository at this point in the history
…st node labels to integers (#546)

* osmnx compat

* tests

* fix the indent level

* let networkx deal with keys in multigraphs

* fix tests
  • Loading branch information
martinfleis authored Feb 4, 2024
1 parent 1224778 commit 4037c70
Show file tree
Hide file tree
Showing 3 changed files with 35 additions and 10 deletions.
4 changes: 2 additions & 2 deletions momepy/tests/test_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ def test_betweenness_centrality(self):
net2.edges[
(1603226.9576840235, 6464160.158361825),
(1603039.9632033885, 6464087.491175889),
8,
0,
]["betweenness"]
== edge
)
Expand Down Expand Up @@ -176,7 +176,7 @@ def test_mean_nodes(self):
net.edges[
(1603226.9576840235, 6464160.158361825),
(1603039.9632033885, 6464087.491175889),
8,
0,
]["straightness"]
== edge
)
Expand Down
23 changes: 20 additions & 3 deletions momepy/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def test_dataset_missing(self):
with pytest.raises(ValueError, match="The dataset 'sffgkt' is not available."):
mm.datasets.get_path("sffgkt")

def test_gdf_to_nx(self):
def test_gdf_to_nx_warnings(self):
with pytest.warns(
RuntimeWarning, match="The given network does not contain any LineString."
):
Expand All @@ -39,9 +39,16 @@ def test_gdf_to_nx(self):
):
mm.gdf_to_nx(self.df_points_and_linestring)

def test_gdf_to_nx(self):
nx = mm.gdf_to_nx(self.df_streets)
assert nx.number_of_nodes() == 29
assert nx.number_of_edges() == 35
assert nx.nodes[(1603585.6402153103, 6464428.773867372)] == {
"x": 1603585.6402153103,
"y": 6464428.773867372,
}

def test_gdf_to_nx_dual(self):
dual = mm.gdf_to_nx(self.df_streets, approach="dual")
assert dual.number_of_nodes() == 35
assert dual.number_of_edges() == 74
Expand All @@ -60,6 +67,7 @@ def test_gdf_to_nx(self):
assert nx.number_of_nodes() == 29
assert nx.number_of_edges() == 35

def test_gdf_to_nx_directed(self):
nx = mm.gdf_to_nx(self.df_streets, multigraph=False, directed=True)
assert isinstance(nx, networkx.DiGraph)
assert nx.number_of_nodes() == 29
Expand All @@ -78,6 +86,7 @@ def test_gdf_to_nx(self):
with pytest.raises(ValueError, match="Bidirectional lines"):
mm.gdf_to_nx(self.df_streets, directed=False, oneway_column="oneway")

def test_gdf_to_nx_angles(self):
dual = mm.gdf_to_nx(self.df_streets, approach="dual", angles=False)
assert (
dual.edges[
Expand All @@ -98,7 +107,7 @@ def test_gdf_to_nx(self):
dual = mm.gdf_to_nx(
self.df_streets, approach="dual", angles=False, multigraph=False
)
assert isinstance(nx, networkx.Graph)
assert isinstance(dual, networkx.Graph)
assert (
dual.edges[
(1603499.42326969, 6464328.7520580515),
Expand All @@ -108,7 +117,7 @@ def test_gdf_to_nx(self):
)

dual = mm.gdf_to_nx(self.df_streets, approach="dual", multigraph=False)
assert isinstance(nx, networkx.Graph)
assert isinstance(dual, networkx.Graph)
assert dual.edges[
(1603499.42326969, 6464328.7520580515),
(1603510.1061735682, 6464204.555117119),
Expand All @@ -117,6 +126,14 @@ def test_gdf_to_nx(self):
with pytest.raises(ValueError, match="Directed graphs are not supported"):
mm.gdf_to_nx(self.df_streets, approach="dual", directed=True)

def test_gdf_to_nx_labels(self):
nx = mm.gdf_to_nx(self.df_streets, integer_labels=True)
assert nx.number_of_nodes() == 29
assert nx.number_of_edges() == 35
assert nx.nodes[0] == {
"x": 1603585.6402153103,
"y": 6464428.773867372,
}
def test_nx_to_gdf(self):
nx = mm.gdf_to_nx(self.df_streets)
nodes, edges, W = mm.nx_to_gdf(nx, spatial_weights=True)
Expand Down
18 changes: 13 additions & 5 deletions momepy/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,25 +71,25 @@ def _generate_primal(graph, gdf_network, fields, multigraph, oneway_column=None)
stacklevel=3,
)

key = 0
for row in gdf_network.itertuples():
first = row.geometry.coords[0]
last = row.geometry.coords[-1]

data = list(row)[1:]
attributes = dict(zip(fields, data, strict=True))
if multigraph:
graph.add_edge(first, last, key=key, **attributes)
key += 1
graph.add_edge(first, last, **attributes)

if oneway_column:
oneway = bool(getattr(row, oneway_column))
if not oneway:
graph.add_edge(last, first, key=key, **attributes)
key += 1
graph.add_edge(last, first, **attributes)
else:
graph.add_edge(first, last, **attributes)

node_attrs = {node: {"x": node[0], "y": node[1]} for node in graph.nodes}
nx.set_node_attributes(graph, node_attrs)


def _generate_dual(graph, gdf_network, fields, angles, multigraph, angle):
"""Generate a dual graph. Helper for ``gdf_to_nx``."""
Expand Down Expand Up @@ -150,6 +150,7 @@ def gdf_to_nx(
angles=True,
angle="angle",
oneway_column=None,
integer_labels=False,
):
"""
Convert a LineString GeoDataFrame to a ``networkx.MultiGraph`` or other
Expand Down Expand Up @@ -188,6 +189,10 @@ def gdf_to_nx(
path traversal by specifying the boolean column in the GeoDataFrame. Note,
that the reverse conversion ``nx_to_gdf(gdf_to_nx(gdf, directed=True,
oneway_column="oneway"))`` will contain additional duplicated geometries.
integer_labels : bool, default False
Convert node labels to integers. By default, node labels are tuples with (x, y)
coordinates. Set to True to encode them as integers. Note that the x, and y
coordinates are always preserved as node attributes.
Returns
-------
Expand Down Expand Up @@ -273,6 +278,9 @@ def gdf_to_nx(
f"Approach '{approach}' is not supported. Use 'primal' or 'dual'."
)

if integer_labels:
net = nx.convert_node_labels_to_integers(net)

return net


Expand Down

0 comments on commit 4037c70

Please sign in to comment.