Source code for iduedu.graph.components

import warnings
from typing import Any, Literal

import numpy as np

from iduedu._numba.components import connected_components_numba, strongly_connected_components_numba
from iduedu._numba.csr import sparse_row2numba_bool_matrix
from iduedu.graph.adjacency import build_boolean_adjacency_matrix
from iduedu.graph.urban_graph import UrbanGraph

ComponentMode = Literal["auto", "connected", "weak", "strong"]


def _validate_graph(graph: UrbanGraph) -> None:
    if not isinstance(graph, UrbanGraph):
        raise TypeError(f"graph must be UrbanGraph, got {type(graph).__name__}")


def _components_from_labels(nodelist: list[Any], labels: np.ndarray) -> list[set[Any]]:
    def sort_components(components: list[set[Any]]) -> list[set[Any]]:
        """Sort components by descending size and stable textual node order."""
        return sorted(
            components, key=lambda component: (-len(component), min(map(repr, component)) if component else "")
        )

    components_by_label: dict[int, set[Any]] = {}
    for pos, label in enumerate(labels):
        components_by_label.setdefault(int(label), set()).add(nodelist[pos])
    return sort_components(list(components_by_label.values()))


[docs] def connected_components(graph: UrbanGraph) -> list[set[Any]]: """ Return connected components of an undirected ``UrbanGraph``. For directed graphs use :func:`weakly_connected_components` or :func:`strongly_connected_components` explicitly. See also: https://iduclub.github.io/IduEdu/examples/connectivity.html """ _validate_graph(graph) if graph.is_directed: raise ValueError("connected_components is not defined for directed UrbanGraph; use weak or strong components") nodelist = graph.nodes_gdf.index.to_list() adjacency = build_boolean_adjacency_matrix(graph, nodelist=nodelist, weak=True) return _components_from_labels(nodelist, connected_components_numba(sparse_row2numba_bool_matrix(adjacency)))
[docs] def weakly_connected_components(graph: UrbanGraph) -> list[set[Any]]: """ Return weakly connected components. Edge directions are ignored. For undirected graphs this is equivalent to :func:`connected_components`. See also: https://iduclub.github.io/IduEdu/examples/connectivity.html """ _validate_graph(graph) nodelist = graph.nodes_gdf.index.to_list() adjacency = build_boolean_adjacency_matrix(graph, nodelist=nodelist, weak=True) return _components_from_labels(nodelist, connected_components_numba(sparse_row2numba_bool_matrix(adjacency)))
[docs] def strongly_connected_components(graph: UrbanGraph) -> list[set[Any]]: """ Return strongly connected components. For undirected graphs this is equivalent to connected components and emits a warning. See also: https://iduclub.github.io/IduEdu/examples/connectivity.html """ _validate_graph(graph) if not graph.is_directed: warnings.warn( "strongly_connected_components called for an undirected UrbanGraph; " "returning connected components instead.", UserWarning, stacklevel=2, ) return connected_components(graph) nodelist = graph.nodes_gdf.index.to_list() adjacency = build_boolean_adjacency_matrix(graph, nodelist=nodelist, weak=False) labels = strongly_connected_components_numba( sparse_row2numba_bool_matrix(adjacency), sparse_row2numba_bool_matrix(adjacency.T.tocsr()), ) return _components_from_labels(nodelist, labels)
[docs] def number_connected_components(graph: UrbanGraph) -> int: """Return the number of connected components in an undirected graph. See also: https://iduclub.github.io/IduEdu/examples/connectivity.html """ return len(connected_components(graph))
[docs] def number_weakly_connected_components(graph: UrbanGraph) -> int: """Return the number of weakly connected components. See also: https://iduclub.github.io/IduEdu/examples/connectivity.html """ return len(weakly_connected_components(graph))
[docs] def number_strongly_connected_components(graph: UrbanGraph) -> int: """Return the number of strongly connected components. See also: https://iduclub.github.io/IduEdu/examples/connectivity.html """ return len(strongly_connected_components(graph))
[docs] def largest_connected_component(graph: UrbanGraph) -> set[Any]: """Return the largest connected component of an undirected graph. See also: https://iduclub.github.io/IduEdu/examples/connectivity.html """ components = connected_components(graph) return components[0] if components else set()
[docs] def largest_weakly_connected_component(graph: UrbanGraph) -> set[Any]: """Return the largest weakly connected component. See also: https://iduclub.github.io/IduEdu/examples/connectivity.html """ components = weakly_connected_components(graph) return components[0] if components else set()
[docs] def largest_strongly_connected_component(graph: UrbanGraph) -> set[Any]: """Return the largest strongly connected component. See also: https://iduclub.github.io/IduEdu/examples/connectivity.html """ components = strongly_connected_components(graph) return components[0] if components else set()
[docs] def largest_component(graph: UrbanGraph, mode: ComponentMode = "auto") -> set[Any]: """Return the largest component according to the selected connectivity mode. Args: graph: Graph to inspect. mode: Connectivity mode. ``"auto"`` selects ``"strong"`` for directed graphs and ``"connected"`` for undirected graphs. Returns: Set of node ids in the largest component. Returns an empty set for an empty graph. Raises: ValueError: If ``mode`` is not supported. See also: https://iduclub.github.io/IduEdu/examples/connectivity.html """ _validate_graph(graph) if mode == "auto": mode = "strong" if graph.is_directed else "connected" if mode == "connected": return largest_connected_component(graph) if mode == "weak": return largest_weakly_connected_component(graph) if mode == "strong": return largest_strongly_connected_component(graph) raise ValueError(f"mode must be 'auto', 'connected', 'weak' or 'strong', got {mode!r}")