2

I have dataset from which I have constructed a NetworkX compatible graph. A shapefile has been converted to dictionaries for nodes and edges, which has then been converted to a GeoDataFrame. From there on, I have used ox.graph_from_gdfs() to create a functioning graph. The edge GeoDataFrame looks something like this (first row, simplified):

            | id     | ref  | name  | speedlim | length|  geometry                          | u    | v   | key
1193,2716,0 | 11452  | ref1 | name1 | 50       | 15    |  LINESTRING (10.5 60.4, 10.5 60.4) | 1193 | 2716| 0

while the node GeoDataFrame looks like this:

       | x    | y     | id    | geometry     
111604 | 10.5 | 60.4  | 11604 | POINT (10.5 60.4)

Converting these to MultiDiGraph returns no errors:

G = ox.graph_from_gdfs(gdf_nodes, gdf_edges)

Same data is also returned when converting back from graph to gdfs.

However, when simplifying G, the following error is raised:

G = ox.simplify_graph(G)

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-18-e400610fe7d3> in <module>
----> 1 F = ox.simplify_graph(G)

~\anaconda3\envs\ox\lib\site-packages\osmnx\simplification.py in simplify_graph(G, strict, remove_rings)
    276         for key in edge_attributes:
    277             # don't touch the length attribute, we'll sum it at the end
--> 278             if len(set(edge_attributes[key])) == 1 and not key == "length":
    279                 # if there's only 1 unique value in this attribute list,
    280                 # consolidate it to the single value (the zero-th)

**TypeError: unhashable type: 'LineString'**

My guess would be that parts of the data in gdf_nodes and gdf_edges are not in the correct format, or that something is missing. However, I can't figure out what. I have not encountered any other errors with OSMnx apart from when using this function.


EDIT 1:

Here is a simple code to reproduce the error

import geopandas as gpd
import osmnx as ox
import networkx as nx
from shapely.geometry import Point, LineString


# Sample dictionary containing edge data (copy from first elements in dataset)
edges_test = {
    (111603,111604,0) : {"id": 11452, "ref":"Mohagavegen", "name":"Mohagavegen", "speedlim":50, "length":15.1, "geometry":LineString([(10.55351,60.40720), (10.55375,60.40714)]), "u":111603, "v":111604, "key":0},

    (111604,111605,0) : {"id": 11453, "ref":"Mohagavegen", "name":"Mohagavegen", "speedlim":50, "length":120.8, "geometry":LineString([Point(10.553752594 ,60.407140812), Point(10.554987804,60.406802271), Point(10.555623630,60.406579470)]), "u":111604, "v":111605, "key":0},

    (111605,111606,0) : {"id": 11454, "ref":"Mohagavegen", "name":"Mohagavegen", "speedlim":50, "length":14.2, "geometry":LineString([Point(10.55562 ,60.40658), Point(10.55584 ,60.40651)]), "u":111605, "v":111606, "key":0}
}


# Sample dictionary containing node data (copy from first elements in dataset)
nodes_test = {
    11603: {"x":10.5538, "y":60.4071, "id":111603, "geometry":Point((10.55375,60.40714))},
    11604: {"x":10.5538, "y":60.4071, "id":111604, "geometry":Point((10.55375,60.40714))},
    11605: {"x":10.5556, "y":60.4066, "id":111605, "geometry":Point((10.5556,60.4066))},
    11606: {"x":10.5558, "y":60.4065, "id":111606, "geometry":Point((10.5558,60.4065))}
}


# Convert edges into geodataframe
gdf_edges = gpd.GeoDataFrame(edges_test, crs = crs).T
gdf_edges = gpd.GeoDataFrame(
    edges_df, geometry=gdf_edges['geometry'])

# Convert nodes into geodataframe
gdf_nodes = gpd.GeoDataFrame(nodes_test, crs = crs).T
gdf_nodes = gpd.GeoDataFrame(
    nodes_df, geometry=gdf_nodes['geometry'])

# Build graph from geodataframes 
F = ox.graph_from_gdfs(gdf_nodes, gdf_edges)

# Plotting will show that there is one intersectial node present
# ox.plot_graph(F)

# Simplify graph
F = ox.simplify_graph(F)

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-113-f81732e4921a> in <module>
     41 
     42 # Simplify graph
---> 43 F = ox.simplify_graph(F)

~\anaconda3\envs\ox\lib\site-packages\osmnx\simplification.py in simplify_graph(G, strict, remove_rings)
    276         for key in edge_attributes:
    277             # don't touch the length attribute, we'll sum it at the end
--> 278             if len(set(edge_attributes[key])) == 1 and not key == "length":
    279                 # if there's only 1 unique value in this attribute list,
    280                 # consolidate it to the single value (the zero-th)

TypeError: unhashable type: 'LineString'


I suspect there are some duplicated nodes with different IDs (see x,y for 111603 and 111604). Maybe this could be the issue?

Samuel
  • 211
  • 2
  • 10
  • 1
    Please provide a complete minimal reproducible code snippet. – gboeing Jan 04 '21 at 20:58
  • Note that you can only simplify a graph *once*. Subsequent attempts will throw an error. My best guess without a reproducible code snippet is that's what's happening here. Also, it's a best practice to pass the `graph_attrs` argument to the `graph_from_gdfs` function: see https://stackoverflow.com/a/65501746/7321942 for an example. – gboeing Jan 05 '21 at 04:31
  • I've added code to reproduce the error; please see edit 1 in original post. Graph hasn't been simplified because it's not retrieved with OSMnx, but rather built from a local dataset. – Samuel Jan 05 '21 at 08:08
  • 1
    The problem is that simplify tries to create a new attribute for the simplified edge. To do that it needs to hash all the values, or the columns in the original gdf. Since LineString isn't hashable (https://github.com/Toblerity/Shapely/issues/209) that doesn't work. You would need to drop the geometry column if you need to simplify the graph. – obeq Jan 05 '21 at 10:43
  • Yes! That solved it. The function ran flawlessly after removing the geometry attribute – Samuel Jan 06 '21 at 08:54

2 Answers2

1

As pointed out by Obeq, the solution was to remove the attribute containing Linestring.

Following the solution from https://stackoverflow.com/questions/50314296/pythonic-way-to-delete-edge-attributes :

att_list = ['geometry']
for n1, n2, d in G.edges(data=True):
    for att in att_list:
        d.pop(att, None)

# Simplify after removing attribute
G = ox.simplify_graph(G)
Samuel
  • 211
  • 2
  • 10
  • The solution works. However, the topology of the lines are totally ignored in the new graph which causes deformation – Hekd Aug 11 '21 at 16:17
  • @Hekmat, Yes the topological attributes of the edges are discarded. This solution would only be feasible in the case where you would mostly be interested in the connections between the vertices rather than the edges topological properties. – Samuel Aug 16 '21 at 12:52
  • I suggested a modification to the original function so it preserves edges' geometry. The solution is posted as an issue in https://github.com/gboeing/osmnx/issues/744. – Hekd Aug 17 '21 at 13:06
0

I had the same issue. If one follows the solution provided here (dropping the geometry points) the graph gets deformed. So I fix that by modifying the original function 'simplify_graph()'.

  def simplify_graph_modified(G, strict=True, remove_rings=True):
      # define edge segment attributes to sum upon edge simplification
      attrs_to_sum = {"length", "travel_time"}

      # make a copy to not mutate original graph object caller passed in
      G = G.copy()
      initial_node_count = len(G)
      initial_edge_count = len(G.edges)
      all_nodes_to_remove = []
      all_edges_to_add = []
      for path in _get_paths_to_simplify(G, strict=strict):
          path_attributes = dict()
          for u, v in zip(path[:-1], path[1:]):
              edge_count = G.number_of_edges(u, v)
              if edge_count != 1:
                  utils.log(f"Found {edge_count} edges between {u} and {v} when simplifying")
                  
              edge_data = G.edges[u, v, 0]
              edge_data['geometry'] = list(edge_data['geometry'].coords) # -> new code line
              for attr in edge_data:
                  if attr in path_attributes:
                      path_attributes[attr].append(edge_data[attr])
                  else:
                      path_attributes[attr] = [edge_data[attr]]
              #list of lists to one list # -> new line
              path_attributes['geometry'] = sum(path_attributes['geometry'], [])

          # consolidate the path's edge segments' attribute values
          for attr in path_attributes:
              if attr in attrs_to_sum:
                  path_attributes[attr] = sum(path_attributes[attr])
              elif attr == 'geometry': # -> new code line
                  path_attributes[attr] = LineString([Point(node) for node in path_attributes[attr]])
              elif len(set(path_attributes[attr])) == 1:
                  path_attributes[attr] = path_attributes[attr][0]
              else:
                  path_attributes[attr] = list(set(path_attributes[attr]))
  
          # construct the new consolidated edge's geometry for this path
          # -> not required anymore 
          #path_attributes["geometry"] = LineString(
          #    [Point((G.nodes[node]["x"], G.nodes[node]["y"])) for node in path])
          
          # add the nodes and edge to their lists for processing at the end
          all_nodes_to_remove.extend(path[1:-1])
          all_edges_to_add.append(
            {"origin": path[0], "destination": path[-1], "attr_dict": path_attributes})

      # for each edge to add in the list we assembled, create a new edge between
      # the origin and destination
      for edge in all_edges_to_add:
          G.add_edge(edge["origin"], edge["destination"], **edge["attr_dict"])

      # finally remove all the interstitial nodes between the new edges
      G.remove_nodes_from(set(all_nodes_to_remove))

      if remove_rings:
          # remove any connected components that form a self-contained ring
          # without any endpoints
          wccs = nx.weakly_connected_components(G)
          nodes_in_rings = set()
          for wcc in wccs:
              if not any(_is_endpoint(G, n) for n in wcc):
                  nodes_in_rings.update(wcc)
          G.remove_nodes_from(nodes_in_rings)
  return G
Hekd
  • 309
  • 3
  • 10