From e96d6434741190cf825cb96bbc6196f17a999ddd Mon Sep 17 00:00:00 2001 From: Geoff Boeing Date: Tue, 12 Mar 2024 12:51:05 -0700 Subject: [PATCH 1/6] rename simplification endpoint_attrs -> edge_attrs --- osmnx/simplification.py | 44 ++++++++++++++++++++--------------------- tests/test_osmnx.py | 2 +- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/osmnx/simplification.py b/osmnx/simplification.py index f5adba720..3d06e964a 100644 --- a/osmnx/simplification.py +++ b/osmnx/simplification.py @@ -24,7 +24,7 @@ from collections.abc import Iterator -def _is_endpoint(G: nx.MultiDiGraph, node: int, endpoint_attrs: Iterable[str] | None) -> bool: +def _is_endpoint(G: nx.MultiDiGraph, node: int, edge_attrs: Iterable[str] | None) -> bool: """ Determine if a node is a true endpoint of an edge. @@ -40,9 +40,9 @@ def _is_endpoint(G: nx.MultiDiGraph, node: int, endpoint_attrs: Iterable[str] | 3) Or, it does not have exactly two neighbors and degree of 2 or 4. - 4) Or, if `endpoint_attrs` is not None, and its incident edges have + 4) Or, if `edge_attrs` is not None, and its incident edges have different values than each other for any of the edge attributes in - `endpoint_attrs`. + `edge_attrs`. Parameters ---------- @@ -50,11 +50,11 @@ def _is_endpoint(G: nx.MultiDiGraph, node: int, endpoint_attrs: Iterable[str] | Input graph. node The node to examine. - endpoint_attrs + edge_attrs Edge attribute names for relaxing the strictness of endpoint determination. If not None, a node is an endpoint if its incident edges have different values then each other for any of the edge - attributes in `endpoint_attrs`. + attributes in `edge_attrs`. Returns ------- @@ -89,8 +89,8 @@ def _is_endpoint(G: nx.MultiDiGraph, node: int, endpoint_attrs: Iterable[str] | # each attribute to check, collect the attribute's values in all inbound # and outbound edges. if there is more than 1 unique value then then this # node is an endpoint - if endpoint_attrs is not None: - for attr in endpoint_attrs: + if edge_attrs is not None: + for attr in edge_attrs: in_values = {v for _, _, v in G.in_edges(node, data=attr, keys=False)} out_values = {v for _, _, v in G.out_edges(node, data=attr, keys=False)} if len(in_values | out_values) > 1: @@ -177,7 +177,7 @@ def _build_path( def _get_paths_to_simplify( G: nx.MultiDiGraph, - endpoint_attrs: Iterable[str] | None, + edge_attrs: Iterable[str] | None, ) -> Iterator[list[int]]: """ Generate all the paths to be simplified between endpoint nodes. @@ -189,18 +189,18 @@ def _get_paths_to_simplify( ---------- G Input graph. - endpoint_attrs + edge_attrs Edge attribute names for relaxing the strictness of endpoint determination. If not None, a node is an endpoint if its incident edges have different values then each other for any of the edge - attributes in `endpoint_attrs`. + attributes in `edge_attrs`. Yields ------ path_to_simplify """ # first identify all the nodes that are endpoints - endpoints = {n for n in G.nodes if _is_endpoint(G, n, endpoint_attrs)} + endpoints = {n for n in G.nodes if _is_endpoint(G, n, edge_attrs)} msg = f"Identified {len(endpoints):,} edge endpoints" utils.log(msg, level=lg.INFO) @@ -214,7 +214,7 @@ def _get_paths_to_simplify( yield _build_path(G, endpoint, successor, endpoints) -def _remove_rings(G: nx.MultiDiGraph, endpoint_attrs: Iterable[str] | None) -> nx.MultiDiGraph: +def _remove_rings(G: nx.MultiDiGraph, edge_attrs: Iterable[str] | None) -> nx.MultiDiGraph: """ Remove all self-contained rings from a graph. @@ -225,11 +225,11 @@ def _remove_rings(G: nx.MultiDiGraph, endpoint_attrs: Iterable[str] | None) -> n ---------- G Input graph. - endpoint_attrs + edge_attrs Edge attribute names for relaxing the strictness of endpoint determination. If not None, a node is an endpoint if its incident edges have different values than each other for any of the edge - attributes in `endpoint_attrs`. + attributes in `edge_attrs`. Returns ------- @@ -238,7 +238,7 @@ def _remove_rings(G: nx.MultiDiGraph, endpoint_attrs: Iterable[str] | None) -> n """ nodes_in_rings = set() for wcc in nx.weakly_connected_components(G): - if not any(_is_endpoint(G, n, endpoint_attrs) for n in wcc): + if not any(_is_endpoint(G, n, edge_attrs) for n in wcc): nodes_in_rings.update(wcc) G.remove_nodes_from(nodes_in_rings) return G @@ -247,7 +247,7 @@ def _remove_rings(G: nx.MultiDiGraph, endpoint_attrs: Iterable[str] | None) -> n def simplify_graph( # noqa: PLR0912 G: nx.MultiDiGraph, *, - endpoint_attrs: Iterable[str] | None = None, + edge_attrs: Iterable[str] | None = None, remove_rings: bool = True, track_merged: bool = False, ) -> nx.MultiDiGraph: @@ -265,8 +265,8 @@ def simplify_graph( # noqa: PLR0912 simplified edges can receive a `merged_edges` attribute that contains a list of all the (u, v) node pairs that were merged together. - Use the `endpoint_attrs` parameter to relax simplification strictness. For - example, `endpoint_attrs=["osmid"]` will retain every node whose incident + Use the `edge_attrs` parameter to relax simplification strictness. For + example, `edge_attrs=["osmid"]` will retain every node whose incident edges have different OSM IDs. This lets you keep nodes at elbow two-way intersections (but be aware that sometimes individual blocks have multiple OSM IDs within them too). You could also use this parameter to retain @@ -276,11 +276,11 @@ def simplify_graph( # noqa: PLR0912 ---------- G Input graph. - endpoint_attrs + edge_attrs Edge attribute names for relaxing the strictness of endpoint determination. If not None, a node is an endpoint if its incident edges have different values then each other for any of the edge - attributes in `endpoint_attrs`. + attributes in `edge_attrs`. remove_rings If True, remove isolated self-contained rings that have no endpoints. track_merged @@ -311,7 +311,7 @@ def simplify_graph( # noqa: PLR0912 all_edges_to_add = [] # generate each path that needs to be simplified - for path in _get_paths_to_simplify(G, endpoint_attrs): + for path in _get_paths_to_simplify(G, edge_attrs): # add the interstitial edges we're removing to a list so we can retain # their spatial geometry merged_edges = [] @@ -382,7 +382,7 @@ def simplify_graph( # noqa: PLR0912 G.remove_nodes_from(set(all_nodes_to_remove)) if remove_rings: - G = _remove_rings(G, endpoint_attrs) + G = _remove_rings(G, edge_attrs) # mark the graph as having been simplified G.graph["simplified"] = True diff --git a/tests/test_osmnx.py b/tests/test_osmnx.py index baa147c33..f34b9a43d 100644 --- a/tests/test_osmnx.py +++ b/tests/test_osmnx.py @@ -575,7 +575,7 @@ def test_graph_from() -> None: # graph from polygon G = ox.graph_from_polygon(polygon, network_type="walk", truncate_by_edge=True, simplify=False) - G = ox.simplify_graph(G, endpoint_attrs=["osmid"], remove_rings=False, track_merged=True) + G = ox.simplify_graph(G, edge_attrs=["osmid"], remove_rings=False, track_merged=True) # test custom query filter cf = ( From 034c91ad786459620695f13f3c56379a55d1ec00 Mon Sep 17 00:00:00 2001 From: Geoff Boeing Date: Tue, 12 Mar 2024 14:17:26 -0700 Subject: [PATCH 2/6] add simplification node_attrs function params --- osmnx/simplification.py | 73 ++++++++++++++++++++++++++++++----------- tests/test_osmnx.py | 8 ++++- 2 files changed, 61 insertions(+), 20 deletions(-) diff --git a/osmnx/simplification.py b/osmnx/simplification.py index 3d06e964a..d2d722f7c 100644 --- a/osmnx/simplification.py +++ b/osmnx/simplification.py @@ -24,14 +24,19 @@ from collections.abc import Iterator -def _is_endpoint(G: nx.MultiDiGraph, node: int, edge_attrs: Iterable[str] | None) -> bool: +def _is_endpoint( + G: nx.MultiDiGraph, + node: int, + node_attrs: Iterable[str] | None, + edge_attrs: Iterable[str] | None, +) -> bool: """ Determine if a node is a true endpoint of an edge. Return True if the node is a "true" endpoint of an edge in the network, otherwise False. OpenStreetMap data includes many nodes that exist only as - geometric vertices to allow ways to curve. A true edge endpoint is a node - that satisfies at least 1 of the following 4 rules: + geometric vertices to allow ways to curve. `node` is a true edge endpoint + if it satisfies at least 1 of the following 5 rules: 1) It is its own neighbor (ie, it self-loops). @@ -40,19 +45,25 @@ def _is_endpoint(G: nx.MultiDiGraph, node: int, edge_attrs: Iterable[str] | None 3) Or, it does not have exactly two neighbors and degree of 2 or 4. - 4) Or, if `edge_attrs` is not None, and its incident edges have - different values than each other for any of the edge attributes in - `edge_attrs`. + 4) Or, if `node_attrs` is not None and it has one or more of the + attributes in `node_attrs` + + 5) Or, if `edge_attrs` is not None and its incident edges have different + values than each other for any of the edge attributes in `edge_attrs`. Parameters ---------- G Input graph. node - The node to examine. + The ID of the node. + node_attrs + Node attribute names for relaxing the strictness of endpoint + determination. If not None, the node is an endpoint if it has one or + more of the attributes in `node_attrs`. edge_attrs Edge attribute names for relaxing the strictness of endpoint - determination. If not None, a node is an endpoint if its incident + determination. If not None, the node is an endpoint if its incident edges have different values then each other for any of the edge attributes in `edge_attrs`. @@ -85,6 +96,11 @@ def _is_endpoint(G: nx.MultiDiGraph, node: int, edge_attrs: Iterable[str] | None return True # RULE 4 + # non-strict mode: does it contain an attr denoting that it is an endpoint + if node_attrs is not None and len(set(node_attrs) & G.nodes[node].keys()) > 0: + return True + + # RULE 5 # non-strict mode: do its incident edges have different attr values? for # each attribute to check, collect the attribute's values in all inbound # and outbound edges. if there is more than 1 unique value then then this @@ -177,6 +193,7 @@ def _build_path( def _get_paths_to_simplify( G: nx.MultiDiGraph, + node_attrs: Iterable[str] | None, edge_attrs: Iterable[str] | None, ) -> Iterator[list[int]]: """ @@ -189,6 +206,10 @@ def _get_paths_to_simplify( ---------- G Input graph. + node_attrs + Node attribute names for relaxing the strictness of endpoint + determination. If not None, a node is an endpoint if it has one or + more of the attributes in `node_attrs`. edge_attrs Edge attribute names for relaxing the strictness of endpoint determination. If not None, a node is an endpoint if its incident @@ -200,7 +221,7 @@ def _get_paths_to_simplify( path_to_simplify """ # first identify all the nodes that are endpoints - endpoints = {n for n in G.nodes if _is_endpoint(G, n, edge_attrs)} + endpoints = {n for n in G.nodes if _is_endpoint(G, n, node_attrs, edge_attrs)} msg = f"Identified {len(endpoints):,} edge endpoints" utils.log(msg, level=lg.INFO) @@ -214,7 +235,11 @@ def _get_paths_to_simplify( yield _build_path(G, endpoint, successor, endpoints) -def _remove_rings(G: nx.MultiDiGraph, edge_attrs: Iterable[str] | None) -> nx.MultiDiGraph: +def _remove_rings( + G: nx.MultiDiGraph, + node_attrs: Iterable[str] | None, + edge_attrs: Iterable[str] | None, +) -> nx.MultiDiGraph: """ Remove all self-contained rings from a graph. @@ -225,6 +250,10 @@ def _remove_rings(G: nx.MultiDiGraph, edge_attrs: Iterable[str] | None) -> nx.Mu ---------- G Input graph. + node_attrs + Node attribute names for relaxing the strictness of endpoint + determination. If not None, a node is an endpoint if it has one or + more of the attributes in `node_attrs`. edge_attrs Edge attribute names for relaxing the strictness of endpoint determination. If not None, a node is an endpoint if its incident @@ -238,7 +267,7 @@ def _remove_rings(G: nx.MultiDiGraph, edge_attrs: Iterable[str] | None) -> nx.Mu """ nodes_in_rings = set() for wcc in nx.weakly_connected_components(G): - if not any(_is_endpoint(G, n, edge_attrs) for n in wcc): + if not any(_is_endpoint(G, n, node_attrs, edge_attrs) for n in wcc): nodes_in_rings.update(wcc) G.remove_nodes_from(nodes_in_rings) return G @@ -247,6 +276,7 @@ def _remove_rings(G: nx.MultiDiGraph, edge_attrs: Iterable[str] | None) -> nx.Mu def simplify_graph( # noqa: PLR0912 G: nx.MultiDiGraph, *, + node_attrs: Iterable[str] | None = None, edge_attrs: Iterable[str] | None = None, remove_rings: bool = True, track_merged: bool = False, @@ -265,17 +295,22 @@ def simplify_graph( # noqa: PLR0912 simplified edges can receive a `merged_edges` attribute that contains a list of all the (u, v) node pairs that were merged together. - Use the `edge_attrs` parameter to relax simplification strictness. For - example, `edge_attrs=["osmid"]` will retain every node whose incident - edges have different OSM IDs. This lets you keep nodes at elbow two-way - intersections (but be aware that sometimes individual blocks have multiple - OSM IDs within them too). You could also use this parameter to retain - nodes where sidewalks or bike lanes begin/end in the middle of a block. + Use the `node_attrs` or `edge_attrs` parameters to relax simplification + strictness. For example, `edge_attrs=["osmid"]` will retain every node + whose incident edges have different OSM IDs. This lets you keep nodes at + elbow two-way intersections (but be aware that sometimes individual blocks + have multiple OSM IDs within them too). You could also use this parameter + to retain nodes where sidewalks or bike lanes begin/end in the middle of a + block. Parameters ---------- G Input graph. + node_attrs + Node attribute names for relaxing the strictness of endpoint + determination. If not None, a node is an endpoint if it has one or + more of the attributes in `node_attrs`. edge_attrs Edge attribute names for relaxing the strictness of endpoint determination. If not None, a node is an endpoint if its incident @@ -311,7 +346,7 @@ def simplify_graph( # noqa: PLR0912 all_edges_to_add = [] # generate each path that needs to be simplified - for path in _get_paths_to_simplify(G, edge_attrs): + for path in _get_paths_to_simplify(G, node_attrs, edge_attrs): # add the interstitial edges we're removing to a list so we can retain # their spatial geometry merged_edges = [] @@ -382,7 +417,7 @@ def simplify_graph( # noqa: PLR0912 G.remove_nodes_from(set(all_nodes_to_remove)) if remove_rings: - G = _remove_rings(G, edge_attrs) + G = _remove_rings(G, node_attrs, edge_attrs) # mark the graph as having been simplified G.graph["simplified"] = True diff --git a/tests/test_osmnx.py b/tests/test_osmnx.py index f34b9a43d..7c9662155 100644 --- a/tests/test_osmnx.py +++ b/tests/test_osmnx.py @@ -575,7 +575,13 @@ def test_graph_from() -> None: # graph from polygon G = ox.graph_from_polygon(polygon, network_type="walk", truncate_by_edge=True, simplify=False) - G = ox.simplify_graph(G, edge_attrs=["osmid"], remove_rings=False, track_merged=True) + G = ox.simplify_graph( + G, + node_attrs=["junction", "ref"], + edge_attrs=["osmid"], + remove_rings=False, + track_merged=True, + ) # test custom query filter cf = ( From c6761a9231d1c2410d29213ed842accccba134be Mon Sep 17 00:00:00 2001 From: Geoff Boeing Date: Tue, 12 Mar 2024 14:54:37 -0700 Subject: [PATCH 3/6] improve docstring --- osmnx/simplification.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osmnx/simplification.py b/osmnx/simplification.py index d2d722f7c..02bef26de 100644 --- a/osmnx/simplification.py +++ b/osmnx/simplification.py @@ -291,7 +291,7 @@ def simplify_graph( # noqa: PLR0912 Note that only simplified edges receive a `geometry` attribute. Some of the resulting consolidated edges may comprise multiple OSM ways, and if - so, their multiple attribute values are stored as a list. Optionally, the + so, their unique attribute values are stored as a list. Optionally, the simplified edges can receive a `merged_edges` attribute that contains a list of all the (u, v) node pairs that were merged together. From d61aab1c3d840e18204a3c0f46576f4057cbf35b Mon Sep 17 00:00:00 2001 From: Geoff Boeing Date: Tue, 12 Mar 2024 15:57:50 -0700 Subject: [PATCH 4/6] update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc9f006ce..897dc0107 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,8 @@ Read the v2 [migration guide](https://github.com/gboeing/osmnx/issues/1123) - 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) +- add node_attrs argument to simplification.simplify_graph function to flexibly relax strictness (#1145) +- rename simplification.simplify_graph endpoint_attrs argument to edge_attrs (#1145) - rename truncate.truncate_graph_dist max_dist argument to dist for consistency with rest of package (#1134) - rename settings module's default_accept_language, default_referer, and default_user_agent settings (#1129) - rename settings module's memory, nominatim_endpoint, overpass_endpoint, and timeout settings (#1136) From be80e05aea40d54e77868237bfb7b8c1d96b8b26 Mon Sep 17 00:00:00 2001 From: Geoff Boeing Date: Wed, 13 Mar 2024 12:04:16 -0700 Subject: [PATCH 5/6] rename node_attrs -> node_attrs_include and edge_attrs -> edge_attrs_differ, improve docstrings and comments --- osmnx/simplification.py | 138 +++++++++++++++++++++------------------- tests/test_osmnx.py | 4 +- 2 files changed, 73 insertions(+), 69 deletions(-) diff --git a/osmnx/simplification.py b/osmnx/simplification.py index 02bef26de..4ba6d339c 100644 --- a/osmnx/simplification.py +++ b/osmnx/simplification.py @@ -27,8 +27,8 @@ def _is_endpoint( G: nx.MultiDiGraph, node: int, - node_attrs: Iterable[str] | None, - edge_attrs: Iterable[str] | None, + node_attrs_include: Iterable[str] | None, + edge_attrs_differ: Iterable[str] | None, ) -> bool: """ Determine if a node is a true endpoint of an edge. @@ -45,11 +45,12 @@ def _is_endpoint( 3) Or, it does not have exactly two neighbors and degree of 2 or 4. - 4) Or, if `node_attrs` is not None and it has one or more of the - attributes in `node_attrs` + 4) Or, if `node_attrs_include` is not None and it has one or more of the + attributes in `node_attrs_include`. - 5) Or, if `edge_attrs` is not None and its incident edges have different - values than each other for any of the edge attributes in `edge_attrs`. + 5) Or, if `edge_attrs_differ` is not None and its incident edges have + different values than each other for any of the edge attributes in + `edge_attrs_differ`. Parameters ---------- @@ -57,15 +58,15 @@ def _is_endpoint( Input graph. node The ID of the node. - node_attrs + node_attrs_include Node attribute names for relaxing the strictness of endpoint - determination. If not None, the node is an endpoint if it has one or - more of the attributes in `node_attrs`. - edge_attrs + determination. If not None, a node is always an endpoint if it has one + or more of the attributes in `node_attrs_include`. + edge_attrs_differ Edge attribute names for relaxing the strictness of endpoint - determination. If not None, the node is an endpoint if its incident - edges have different values then each other for any of the edge - attributes in `edge_attrs`. + determination. If not None, a node is always an endpoint if its + incident edges have different values than each other for any of the + edge attributes in `edge_attrs_differ`. Returns ------- @@ -97,16 +98,16 @@ def _is_endpoint( # RULE 4 # non-strict mode: does it contain an attr denoting that it is an endpoint - if node_attrs is not None and len(set(node_attrs) & G.nodes[node].keys()) > 0: + if node_attrs_include is not None and len(set(node_attrs_include) & G.nodes[node].keys()) > 0: return True # RULE 5 # non-strict mode: do its incident edges have different attr values? for # each attribute to check, collect the attribute's values in all inbound - # and outbound edges. if there is more than 1 unique value then then this - # node is an endpoint - if edge_attrs is not None: - for attr in edge_attrs: + # and outbound edges. if there is more than 1 unique value then this node + # is an endpoint + if edge_attrs_differ is not None: + for attr in edge_attrs_differ: in_values = {v for _, _, v in G.in_edges(node, data=attr, keys=False)} out_values = {v for _, _, v in G.out_edges(node, data=attr, keys=False)} if len(in_values | out_values) > 1: @@ -193,8 +194,8 @@ def _build_path( def _get_paths_to_simplify( G: nx.MultiDiGraph, - node_attrs: Iterable[str] | None, - edge_attrs: Iterable[str] | None, + node_attrs_include: Iterable[str] | None, + edge_attrs_differ: Iterable[str] | None, ) -> Iterator[list[int]]: """ Generate all the paths to be simplified between endpoint nodes. @@ -206,22 +207,22 @@ def _get_paths_to_simplify( ---------- G Input graph. - node_attrs + node_attrs_include Node attribute names for relaxing the strictness of endpoint - determination. If not None, a node is an endpoint if it has one or - more of the attributes in `node_attrs`. - edge_attrs + determination. If not None, a node is always an endpoint if it has one + or more of the attributes in `node_attrs_include`. + edge_attrs_differ Edge attribute names for relaxing the strictness of endpoint - determination. If not None, a node is an endpoint if its incident - edges have different values then each other for any of the edge - attributes in `edge_attrs`. + determination. If not None, a node is always an endpoint if its + incident edges have different values than each other for any of the + edge attributes in `edge_attrs_differ`. Yields ------ path_to_simplify """ # first identify all the nodes that are endpoints - endpoints = {n for n in G.nodes if _is_endpoint(G, n, node_attrs, edge_attrs)} + endpoints = {n for n in G.nodes if _is_endpoint(G, n, node_attrs_include, edge_attrs_differ)} msg = f"Identified {len(endpoints):,} edge endpoints" utils.log(msg, level=lg.INFO) @@ -237,47 +238,47 @@ def _get_paths_to_simplify( def _remove_rings( G: nx.MultiDiGraph, - node_attrs: Iterable[str] | None, - edge_attrs: Iterable[str] | None, + node_attrs_include: Iterable[str] | None, + edge_attrs_differ: Iterable[str] | None, ) -> nx.MultiDiGraph: """ - Remove all self-contained rings from a graph. + Remove all graph components that consist only of a single chordless cycle. - This identifies any connected components that form a self-contained ring - without any endpoints, and removes them from the graph. + This identifies all connected components in the graph that consist only of + a single isolated self-contained ring, and removes them from the graph. Parameters ---------- G Input graph. - node_attrs + node_attrs_include Node attribute names for relaxing the strictness of endpoint - determination. If not None, a node is an endpoint if it has one or - more of the attributes in `node_attrs`. - edge_attrs + determination. If not None, a node is always an endpoint if it has one + or more of the attributes in `node_attrs_include`. + edge_attrs_differ Edge attribute names for relaxing the strictness of endpoint - determination. If not None, a node is an endpoint if its incident - edges have different values than each other for any of the edge - attributes in `edge_attrs`. + determination. If not None, a node is always an endpoint if its + incident edges have different values than each other for any of the + edge attributes in `edge_attrs_differ`. Returns ------- G - Graph with self-contained rings removed. + Graph with all chordless cycle components removed. """ - nodes_in_rings = set() + to_remove = set() for wcc in nx.weakly_connected_components(G): - if not any(_is_endpoint(G, n, node_attrs, edge_attrs) for n in wcc): - nodes_in_rings.update(wcc) - G.remove_nodes_from(nodes_in_rings) + if not any(_is_endpoint(G, n, node_attrs_include, edge_attrs_differ) for n in wcc): + to_remove.update(wcc) + G.remove_nodes_from(to_remove) return G def simplify_graph( # noqa: PLR0912 G: nx.MultiDiGraph, *, - node_attrs: Iterable[str] | None = None, - edge_attrs: Iterable[str] | None = None, + node_attrs_include: Iterable[str] | None = None, + edge_attrs_differ: Iterable[str] | None = None, remove_rings: bool = True, track_merged: bool = False, ) -> nx.MultiDiGraph: @@ -293,31 +294,34 @@ def simplify_graph( # noqa: PLR0912 the resulting consolidated edges may comprise multiple OSM ways, and if so, their unique attribute values are stored as a list. Optionally, the simplified edges can receive a `merged_edges` attribute that contains a - list of all the (u, v) node pairs that were merged together. - - Use the `node_attrs` or `edge_attrs` parameters to relax simplification - strictness. For example, `edge_attrs=["osmid"]` will retain every node - whose incident edges have different OSM IDs. This lets you keep nodes at - elbow two-way intersections (but be aware that sometimes individual blocks - have multiple OSM IDs within them too). You could also use this parameter - to retain nodes where sidewalks or bike lanes begin/end in the middle of a - block. + list of all the `(u, v)` node pairs that were merged together. + + Use the `node_attrs_include` or `edge_attrs_differ` parameters to relax + simplification strictness. For example, `edge_attrs_differ=["osmid"]` will + retain every node whose incident edges have different OSM IDs. This lets + you keep nodes at elbow two-way intersections (but be aware that sometimes + individual blocks have multiple OSM IDs within them too). You could also + use this parameter to retain nodes where sidewalks or bike lanes begin/end + in the middle of a block. Or for example, `node_attrs_include=["highway"]` + will retain every node with a "highway" attribute (regardless of its + value), even if it does not represent a street junction. Parameters ---------- G Input graph. - node_attrs + node_attrs_include Node attribute names for relaxing the strictness of endpoint - determination. If not None, a node is an endpoint if it has one or - more of the attributes in `node_attrs`. - edge_attrs + determination. If not None, a node is always an endpoint if it has one + or more of the attributes in `node_attrs_include`. + edge_attrs_differ Edge attribute names for relaxing the strictness of endpoint - determination. If not None, a node is an endpoint if its incident - edges have different values then each other for any of the edge - attributes in `edge_attrs`. + determination. If not None, a node is always an endpoint if its + incident edges have different values than each other for any of the + edge attributes in `edge_attrs_differ`. remove_rings - If True, remove isolated self-contained rings that have no endpoints. + If True, remove any graph components that consist only of a single + chordless cycle (i.e., an isolated self-contained ring). track_merged If True, add `merged_edges` attribute on simplified edges, containing a list of all the `(u, v)` node pairs that were merged together. @@ -346,7 +350,7 @@ def simplify_graph( # noqa: PLR0912 all_edges_to_add = [] # generate each path that needs to be simplified - for path in _get_paths_to_simplify(G, node_attrs, edge_attrs): + for path in _get_paths_to_simplify(G, node_attrs_include, edge_attrs_differ): # add the interstitial edges we're removing to a list so we can retain # their spatial geometry merged_edges = [] @@ -417,7 +421,7 @@ def simplify_graph( # noqa: PLR0912 G.remove_nodes_from(set(all_nodes_to_remove)) if remove_rings: - G = _remove_rings(G, node_attrs, edge_attrs) + G = _remove_rings(G, node_attrs_include, edge_attrs_differ) # mark the graph as having been simplified G.graph["simplified"] = True @@ -647,7 +651,7 @@ def _consolidate_intersections_rebuild_graph( # noqa: C901,PLR0912,PLR0915 H.add_node(cluster_label, osmid_original=osmid, **G.nodes[osmid]) else: # if cluster is multiple merged nodes, create one new node with - # attributes to represent them + # attributes to represent the merged nodes' non-null values node_attrs = { "osmid_original": osmids, "x": nodes_subset["x"].iloc[0], diff --git a/tests/test_osmnx.py b/tests/test_osmnx.py index 7c9662155..07c367099 100644 --- a/tests/test_osmnx.py +++ b/tests/test_osmnx.py @@ -577,8 +577,8 @@ def test_graph_from() -> None: G = ox.graph_from_polygon(polygon, network_type="walk", truncate_by_edge=True, simplify=False) G = ox.simplify_graph( G, - node_attrs=["junction", "ref"], - edge_attrs=["osmid"], + node_attrs_include=["junction", "ref"], + edge_attrs_differ=["osmid"], remove_rings=False, track_merged=True, ) From 11f4e3c51e22f8712eb5c6b64f364ce056cbf2f3 Mon Sep 17 00:00:00 2001 From: Geoff Boeing Date: Wed, 13 Mar 2024 12:17:12 -0700 Subject: [PATCH 6/6] update changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 897dc0107..2684fc70f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,8 +25,8 @@ Read the v2 [migration guide](https://github.com/gboeing/osmnx/issues/1123) - 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) -- add node_attrs argument to simplification.simplify_graph function to flexibly relax strictness (#1145) -- rename simplification.simplify_graph endpoint_attrs argument to edge_attrs (#1145) +- add node_attrs_include argument to simplification.simplify_graph function to flexibly relax strictness (#1145) +- rename simplification.simplify_graph endpoint_attrs argument to edge_attrs_differ (#1145) - rename truncate.truncate_graph_dist max_dist argument to dist for consistency with rest of package (#1134) - rename settings module's default_accept_language, default_referer, and default_user_agent settings (#1129) - rename settings module's memory, nominatim_endpoint, overpass_endpoint, and timeout settings (#1136)