Source code for iduedu.graph.graph_inputs

from typing import Any, Iterable

import geopandas as gpd
import numpy as np
import pandas as pd

from iduedu.graph.urban_graph import UrbanGraph


[docs] def nearest_nodes( urban_graph: UrbanGraph, gdf: gpd.GeoDataFrame, *, graph_node_column: str = "graph_node_id", ) -> pd.Series: """Return the nearest graph node id for each input geometry. Args: urban_graph: Graph whose ``nodes_gdf`` contains point geometries. gdf: GeoDataFrame with geometries to match to graph nodes. graph_node_column: Name assigned to the returned ``Series``. Returns: Series indexed like ``gdf`` with nearest node ids as values. Raises: TypeError: If ``gdf`` is not a GeoDataFrame. ValueError: If graph or object geometries cannot be matched safely. See also: https://iduclub.github.io/IduEdu/examples/objects_and_nearest_nodes.html """ if not isinstance(gdf, gpd.GeoDataFrame): raise TypeError(f"gdf must be GeoDataFrame, got {type(gdf).__name__}") if gdf.empty: raise ValueError("gdf must not be empty") if gdf.index.has_duplicates: raise ValueError("gdf.index must be unique") if gdf.crs is None: raise ValueError("gdf must have CRS to calculate nearest graph nodes") nodes_gdf = urban_graph.nodes_gdf if not isinstance(nodes_gdf, gpd.GeoDataFrame): raise TypeError("UrbanGraph nodes_gdf must be GeoDataFrame to calculate nearest graph nodes") if nodes_gdf.empty: raise ValueError("graph is empty") if nodes_gdf.index.has_duplicates: raise ValueError("graph.nodes_gdf.index must be unique") local_crs = nodes_gdf.crs if local_crs is None: local_crs = getattr(urban_graph.edges_gdf, "crs", None) if local_crs is None: raise ValueError("UrbanGraph does not have CRS on nodes_gdf or edges_gdf") points_geom = gdf.geometry if points_geom.crs != local_crs: points_geom = points_geom.to_crs(local_crs) points_geom = points_geom.representative_point() matches = nodes_gdf.sindex.nearest(points_geom.values, return_all=False) order = np.argsort(matches[0], kind="stable") nearest_positions = matches[1][order] if len(nearest_positions) != len(gdf): raise ValueError("Could not find nearest graph node for some input geometries") node_ids = nodes_gdf.index.to_numpy()[nearest_positions] return pd.Series(node_ids, index=gdf.index, name=graph_node_column)
def resolve_graph_nodes_input( *, urban_graph: UrbanGraph, nodes: Iterable[Any] | None, gdf: gpd.GeoDataFrame | pd.DataFrame | None, graph_node_column: str, nodes_name: str, gdf_name: str, ) -> pd.Series: """Normalize ``nodes`` or ``gdf`` inputs into graph node ids. If ``gdf`` already contains ``graph_node_column``, values are read from that column. Otherwise ``gdf`` must be a GeoDataFrame and each geometry is matched to the nearest graph node. Plain ``nodes`` input is validated and returned as a Series indexed by node ids. """ if nodes is not None and gdf is not None: raise ValueError(f"Pass either {nodes_name} or {gdf_name}, not both") if nodes is None and gdf is None: raise ValueError(f"Pass {nodes_name} or {gdf_name}") if not isinstance(urban_graph, UrbanGraph): raise TypeError(f"graph must be UrbanGraph, got {type(urban_graph).__name__}") if len(urban_graph.nodes_gdf) == 0: raise ValueError("graph is empty") graph_node_index = urban_graph.nodes_gdf.index if graph_node_index.has_duplicates: raise ValueError("graph.nodes_gdf.index must be unique") if gdf is not None: if not isinstance(gdf, (gpd.GeoDataFrame, pd.DataFrame)): raise TypeError(f"{gdf_name} must be DataFrame or GeoDataFrame, got {type(gdf).__name__}") if gdf.empty: raise ValueError(f"{gdf_name} must not be empty") if gdf.index.has_duplicates: raise ValueError(f"{gdf_name}.index must be unique") if graph_node_column in gdf.columns: if gdf[graph_node_column].isna().any(): raise ValueError(f"{gdf_name}[{graph_node_column!r}] contains NaN") result = gdf[graph_node_column].copy() else: if not isinstance(gdf, gpd.GeoDataFrame): raise KeyError(f"{gdf_name} has no node column {graph_node_column!r} and no geometry") result = nearest_nodes(urban_graph, gdf, graph_node_column=graph_node_column) else: resolved_nodes = list(nodes) if not resolved_nodes: raise ValueError(f"{nodes_name} must not be empty") if pd.isna(pd.Series(resolved_nodes)).any(): raise ValueError(f"{nodes_name} must not contain NaN") result = pd.Series(resolved_nodes, index=pd.Index(resolved_nodes), name=graph_node_column) missing_nodes = [node for node in result.to_numpy() if node not in graph_node_index] if missing_nodes: preview = missing_nodes[:10] raise ValueError( f"{nodes_name} contain nodes that are absent in graph: {preview}" + (" ..." if len(missing_nodes) > 10 else "") ) return result