diff --git a/city2graph/__init__.py b/city2graph/__init__.py index 9cf313b..a147b5d 100644 --- a/city2graph/__init__.py +++ b/city2graph/__init__.py @@ -31,6 +31,7 @@ # Explicit re-export to preserve typing information for mypy on public API from .mobility import * # noqa: F403 from .morphology import * # noqa: F403 +from .networks import * # noqa: F403 from .proximity import * # noqa: F403 from .transportation import * # noqa: F403 from .utils import * # noqa: F403 diff --git a/city2graph/networks.py b/city2graph/networks.py new file mode 100644 index 0000000..c3c9182 --- /dev/null +++ b/city2graph/networks.py @@ -0,0 +1,545 @@ +""" +OSM and SUMO network file import utilities. + +Functions in this module load road network files from OpenStreetMap (.osm, .osm.gz) +and SUMO (.net.xml) formats into GeoDataFrames and optionally NetworkX graphs, +following the same (nodes_gdf, edges_gdf) convention used throughout city2graph. +""" + +from __future__ import annotations + +import logging +import xml.etree.ElementTree as ET +from pathlib import Path +from typing import TYPE_CHECKING + +import geopandas as gpd +import networkx as nx +from shapely.geometry import LineString +from shapely.geometry import Point + +if TYPE_CHECKING: + pass + +logger = logging.getLogger(__name__) + +__all__ = ["load_osm_network", "load_sumo_network"] + + +def _network_gdfs_to_nx( + nodes_gdf: gpd.GeoDataFrame, + edges_gdf: gpd.GeoDataFrame, + *, + directed: bool, +) -> nx.DiGraph | nx.Graph: + graph: nx.DiGraph | nx.Graph = nx.DiGraph() if directed else nx.Graph() + graph.graph["crs"] = str(nodes_gdf.crs) if nodes_gdf.crs else None + for node_id, row in nodes_gdf.iterrows(): + graph.add_node(node_id, **row.to_dict()) + for (src, dst), row in edges_gdf.iterrows(): + graph.add_edge(src, dst, **row.to_dict()) + return graph + + +# --------------------------------------------------------------------------- +# OSM +# --------------------------------------------------------------------------- + + +def _osmnx_to_gdfs( + G: nx.MultiDiGraph, + *, + directed: bool, +) -> tuple[gpd.GeoDataFrame, gpd.GeoDataFrame]: + import osmnx as ox + + nodes_gdf, edges_gdf = ox.graph_to_gdfs(G) + nodes_gdf = nodes_gdf.to_crs("EPSG:4326") + edges_gdf = edges_gdf.to_crs("EPSG:4326") + + # Drop the osmnx 'key' level from the (u, v, key) MultiIndex. + edges_gdf = edges_gdf.reset_index() + + if directed: + edges_gdf = edges_gdf.drop_duplicates(subset=["u", "v"]).set_index(["u", "v"]) + else: + # Canonicalize direction so u <= v, then deduplicate. + swap = edges_gdf["u"] > edges_gdf["v"] + edges_gdf.loc[swap, ["u", "v"]] = edges_gdf.loc[swap, ["v", "u"]].to_numpy() + edges_gdf = edges_gdf.drop_duplicates(subset=["u", "v"]).set_index(["u", "v"]) + + return nodes_gdf, edges_gdf + + +def load_osm_network( + path: str | Path, + *, + retain_all: bool = True, + simplify: bool = True, + as_nx: bool = False, + directed: bool = False, +) -> tuple[gpd.GeoDataFrame, gpd.GeoDataFrame] | nx.DiGraph | nx.Graph: + """ + Load an OpenStreetMap network file into GeoDataFrames or a NetworkX graph. + + Supports ``.osm`` and ``.osm.gz`` formats via osmnx. The result follows + the same ``(nodes_gdf, edges_gdf)`` convention used throughout city2graph. + + Parameters + ---------- + path : str | Path + Path to an OSM file (``.osm`` or ``.osm.gz``). + retain_all : bool, default=True + If ``True``, retain all graph components including disconnected islands. + simplify : bool, default=True + If ``True``, simplify graph topology by removing interstitial nodes. + as_nx : bool, default=False + If ``True``, return a NetworkX graph instead of GeoDataFrames. + directed : bool, default=False + If ``True``, return a directed graph preserving OSM edge direction. + If ``False``, each undirected road is represented once with + ``u <= v`` canonical ordering. + + Returns + ------- + tuple[gpd.GeoDataFrame, gpd.GeoDataFrame] | nx.DiGraph | nx.Graph + ``(nodes_gdf, edges_gdf)`` when ``as_nx`` is ``False``; otherwise a + ``nx.DiGraph`` (``directed=True``) or ``nx.Graph`` (``directed=False``). + Nodes are indexed by OSM node id. Edges are indexed by ``(u, v)``. + Both GeoDataFrames use CRS ``EPSG:4326``. + + Raises + ------ + ImportError + If osmnx is not installed. + FileNotFoundError + If ``path`` does not exist. + ValueError + If the file cannot be parsed as a valid OSM network. + """ + try: + import osmnx as ox + except ImportError as exc: + msg = "osmnx is required for load_osm_network. Install it with: pip install osmnx" + raise ImportError(msg) from exc + + file_path = Path(path) + if not file_path.exists(): + msg = f"OSM file not found: {file_path}" + raise FileNotFoundError(msg) + + try: + G: nx.MultiDiGraph = ox.graph_from_xml( + file_path, + retain_all=retain_all, + simplify=simplify, + bidirectional=directed, + ) + except Exception as exc: + msg = f"Failed to parse OSM file {file_path}: {exc}" + raise ValueError(msg) from exc + + nodes_gdf, edges_gdf = _osmnx_to_gdfs(G, directed=directed) + + if as_nx: + return _network_gdfs_to_nx(nodes_gdf, edges_gdf, directed=directed) + return nodes_gdf, edges_gdf + + +# --------------------------------------------------------------------------- +# SUMO +# --------------------------------------------------------------------------- + + +def _parse_sumo_location( + location_elem: ET.Element | None, +) -> tuple[float, float, str | None]: + """ + Extract coordinate offset and projection string from a SUMO ```` element. + + Parameters + ---------- + location_elem : ET.Element | None + The ```` XML element, or ``None`` when absent. + + Returns + ------- + tuple[float, float, str | None] + ``(offset_x, offset_y, proj_string)`` where offsets are the values from + ``netOffset`` and ``proj_string`` is ``None`` when unavailable or ``"!"``. + """ + if location_elem is None: + return 0.0, 0.0, None + + net_offset = location_elem.get("netOffset", "0.00,0.00") + proj_param = location_elem.get("projParameter", "") + + try: + ox_str, oy_str = net_offset.split(",") + offset_x, offset_y = float(ox_str), float(oy_str) + except (ValueError, AttributeError): + offset_x, offset_y = 0.0, 0.0 + + proj_string = proj_param.strip() if proj_param.strip() not in {"", "!"} else None + return offset_x, offset_y, proj_string + + +def _sumo_xy_to_lonlat( + x: float, + y: float, + *, + offset_x: float, + offset_y: float, + proj_string: str | None, +) -> tuple[float, float]: + """ + Convert a SUMO local ``(x, y)`` coordinate to WGS84 ``(lon, lat)``. + + SUMO net coordinates satisfy ``sumo = projected - netOffset``, so the + inverse is ``projected = sumo + netOffset``, followed by the inverse + cartographic projection. + + Parameters + ---------- + x : float + SUMO net x-coordinate (metres in local CRS). + y : float + SUMO net y-coordinate (metres in local CRS). + offset_x : float + ``netOffset`` x component. + offset_y : float + ``netOffset`` y component. + proj_string : str | None + PROJ projection string embedded in the SUMO file, or ``None`` when + no projection is defined (coordinates treated as lon/lat directly). + + Returns + ------- + tuple[float, float] + ``(lon, lat)`` in WGS84 degrees. + """ + proj_x = x + offset_x + proj_y = y + offset_y + + if proj_string is None: + return proj_x, proj_y + + try: + from pyproj import Proj + from pyproj import Transformer + + src_proj = Proj(proj_string) + transformer = Transformer.from_proj(src_proj, Proj("EPSG:4326"), always_xy=True) + lon, lat = transformer.transform(proj_x, proj_y) + return float(lon), float(lat) + except Exception: + logger.warning( + "SUMO coordinate reprojection failed for proj=%r; returning offset-corrected values.", + proj_string, + ) + return proj_x, proj_y + + +def _parse_sumo_shape( + shape_str: str, + *, + offset_x: float, + offset_y: float, + proj_string: str | None, +) -> LineString | None: + """ + Parse a SUMO ``shape`` attribute string into a reprojected ``LineString``. + + Parameters + ---------- + shape_str : str + Space-separated ``"x,y"`` pairs from a SUMO shape attribute. + offset_x : float + ``netOffset`` x component. + offset_y : float + ``netOffset`` y component. + proj_string : str | None + PROJ string for reprojection, or ``None``. + + Returns + ------- + LineString | None + Reprojected geometry, or ``None`` when fewer than two valid points exist. + """ + if not shape_str: + return None + try: + coords = [] + for pair in shape_str.strip().split(): + if "," not in pair: + continue + px, py = pair.split(",", 1) + lon, lat = _sumo_xy_to_lonlat( + float(px), + float(py), + offset_x=offset_x, + offset_y=offset_y, + proj_string=proj_string, + ) + coords.append((lon, lat)) + return LineString(coords) if len(coords) >= 2 else None # noqa: TRY300 + except (ValueError, IndexError): + return None + + +def _safe_int(value: str | None) -> int | None: + if value is None: + return None + try: + return int(value) + except ValueError: + return None + + +def _build_sumo_nodes( + root: ET.Element, + *, + offset_x: float, + offset_y: float, + proj_string: str | None, +) -> gpd.GeoDataFrame: + """ + Extract SUMO junctions into a node GeoDataFrame. + + Internal junctions (``type="internal"``) are skipped. + + Parameters + ---------- + root : ET.Element + Root element of the parsed SUMO net XML. + offset_x : float + ``netOffset`` x component. + offset_y : float + ``netOffset`` y component. + proj_string : str | None + PROJ string for reprojection, or ``None``. + + Returns + ------- + gpd.GeoDataFrame + Node records indexed by junction id, CRS ``EPSG:4326``. + """ + records = [] + for junc in root.iter("junction"): + if junc.get("type") == "internal": + continue + junc_id = junc.get("id", "") + try: + raw_x = float(junc.get("x", "0")) + raw_y = float(junc.get("y", "0")) + except ValueError: + continue + lon, lat = _sumo_xy_to_lonlat( + raw_x, + raw_y, + offset_x=offset_x, + offset_y=offset_y, + proj_string=proj_string, + ) + records.append( + { + "node_id": junc_id, + "junction_type": junc.get("type", ""), + "geometry": Point(lon, lat), + } + ) + + if not records: + return gpd.GeoDataFrame( + columns=["node_id", "junction_type", "geometry"], + geometry="geometry", + crs="EPSG:4326", + ).set_index("node_id") + + gdf = gpd.GeoDataFrame(records, geometry="geometry", crs="EPSG:4326") + return gdf.set_index("node_id") + + +def _build_sumo_edges( + root: ET.Element, + *, + offset_x: float, + offset_y: float, + proj_string: str | None, + nodes_gdf: gpd.GeoDataFrame, +) -> gpd.GeoDataFrame: + """ + Extract SUMO edges into an edge GeoDataFrame. + + Internal junction connector edges (id starting with ``":"``) are skipped. + Lane-level speed and length are averaged across lanes per edge. + When no explicit shape is present, a straight line between the endpoint + junctions is used. + + Parameters + ---------- + root : ET.Element + Root element of the parsed SUMO net XML. + offset_x : float + ``netOffset`` x component. + offset_y : float + ``netOffset`` y component. + proj_string : str | None + PROJ string for reprojection, or ``None``. + nodes_gdf : gpd.GeoDataFrame + Node GeoDataFrame used for fallback straight-line geometries. + + Returns + ------- + gpd.GeoDataFrame + Edge records indexed by ``(from_id, to_id)``, CRS ``EPSG:4326``. + """ + records = [] + for edge in root.iter("edge"): + edge_id = edge.get("id", "") + if edge_id.startswith(":"): + continue + from_id = edge.get("from", "") + to_id = edge.get("to", "") + if not from_id or not to_id: + continue + + shape_str = edge.get("shape", "") + geom = _parse_sumo_shape( + shape_str, + offset_x=offset_x, + offset_y=offset_y, + proj_string=proj_string, + ) + if geom is None and from_id in nodes_gdf.index and to_id in nodes_gdf.index: + geom = LineString( + [ + nodes_gdf.loc[from_id, "geometry"], + nodes_gdf.loc[to_id, "geometry"], + ] + ) + + lanes = edge.findall("lane") + num_lanes = len(lanes) + speeds = [float(ln.get("speed")) for ln in lanes if ln.get("speed")] + lengths = [float(ln.get("length")) for ln in lanes if ln.get("length")] + + records.append( + { + "from_id": from_id, + "to_id": to_id, + "edge_id": edge_id, + "num_lanes": num_lanes, + "speed_ms": sum(speeds) / len(speeds) if speeds else None, + "length_m": sum(lengths) / len(lengths) if lengths else None, + "priority": _safe_int(edge.get("priority")), + "edge_type": edge.get("type", ""), + "geometry": geom, + } + ) + + if not records: + return gpd.GeoDataFrame( + columns=[ + "from_id", + "to_id", + "edge_id", + "num_lanes", + "speed_ms", + "length_m", + "priority", + "edge_type", + "geometry", + ], + geometry="geometry", + crs="EPSG:4326", + ).set_index(["from_id", "to_id"]) + + gdf = gpd.GeoDataFrame(records, geometry="geometry", crs="EPSG:4326") + return gdf.set_index(["from_id", "to_id"]) + + +def load_sumo_network( + path: str | Path, + *, + as_nx: bool = False, + directed: bool = True, +) -> tuple[gpd.GeoDataFrame, gpd.GeoDataFrame] | nx.DiGraph | nx.Graph: + """ + Load a SUMO network file (``.net.xml``) into GeoDataFrames or a NetworkX graph. + + SUMO networks use a local Cartesian coordinate system. When the + ```` element contains a valid ``projParameter`` attribute, + coordinates are reprojected to WGS84 (EPSG:4326) via pyproj (available + as a transitive dependency through geopandas). When no projection is + defined (``projParameter="!"``), offset-corrected coordinates are returned + directly and are expected to already be in degrees. + + Parameters + ---------- + path : str | Path + Path to a SUMO network file (``.net.xml``). + as_nx : bool, default=False + If ``True``, return a NetworkX graph instead of GeoDataFrames. + directed : bool, default=True + If ``True``, return a directed graph preserving SUMO edge direction. + SUMO networks are inherently directed; setting this to ``False`` + canonicalises each pair so that ``from_id <= to_id`` and keeps one + representative edge per undirected pair. + + Returns + ------- + tuple[gpd.GeoDataFrame, gpd.GeoDataFrame] | nx.DiGraph | nx.Graph + ``(nodes_gdf, edges_gdf)`` when ``as_nx`` is ``False``; otherwise a + ``nx.DiGraph`` (``directed=True``) or ``nx.Graph`` (``directed=False``). + Nodes are indexed by junction id. Edges are indexed by + ``(from_id, to_id)``. Both GeoDataFrames use CRS ``EPSG:4326``. + + Raises + ------ + FileNotFoundError + If ``path`` does not exist. + ValueError + If the file cannot be parsed as valid SUMO XML. + """ + file_path = Path(path) + if not file_path.exists(): + msg = f"SUMO network file not found: {file_path}" + raise FileNotFoundError(msg) + + try: + tree = ET.parse(file_path) # noqa: S314 + root = tree.getroot() + except ET.ParseError as exc: + msg = f"Failed to parse SUMO network file {file_path}: {exc}" + raise ValueError(msg) from exc + + offset_x, offset_y, proj_string = _parse_sumo_location(root.find("location")) + + nodes_gdf = _build_sumo_nodes( + root, + offset_x=offset_x, + offset_y=offset_y, + proj_string=proj_string, + ) + edges_gdf = _build_sumo_edges( + root, + offset_x=offset_x, + offset_y=offset_y, + proj_string=proj_string, + nodes_gdf=nodes_gdf, + ) + + if not directed and not edges_gdf.empty: + edges_gdf = edges_gdf.reset_index() + swap = edges_gdf["from_id"] > edges_gdf["to_id"] + edges_gdf.loc[swap, ["from_id", "to_id"]] = ( + edges_gdf.loc[swap, ["to_id", "from_id"]].to_numpy() + ) + edges_gdf = edges_gdf.drop_duplicates(subset=["from_id", "to_id"]).set_index( + ["from_id", "to_id"] + ) + + if as_nx: + return _network_gdfs_to_nx(nodes_gdf, edges_gdf, directed=directed) + return nodes_gdf, edges_gdf diff --git a/pyproject.toml b/pyproject.toml index 568327d..45b6e94 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,9 @@ dependencies = [ ] [project.optional-dependencies] +sumo = [ + "sumolib>=1.20.0", +] cpu = [ "torch>=2.9.0", "torchvision>=0.24.0", diff --git a/tests/data/sample.net.xml b/tests/data/sample.net.xml new file mode 100644 index 0000000..6ee378f --- /dev/null +++ b/tests/data/sample.net.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/data/sample.osm b/tests/data/sample.osm new file mode 100644 index 0000000..bd78834 --- /dev/null +++ b/tests/data/sample.osm @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/tests/test_networks.py b/tests/test_networks.py new file mode 100644 index 0000000..9b0a78f --- /dev/null +++ b/tests/test_networks.py @@ -0,0 +1,465 @@ +"""Tests for the networks module (OSM and SUMO file import).""" + +# ruff: noqa: D101, D102, D103 + +from __future__ import annotations + +import sys +import xml.etree.ElementTree as ET +from pathlib import Path + +import geopandas as gpd +import networkx as nx +import pytest +from shapely.geometry import LineString +from shapely.geometry import Point + +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from city2graph.networks import _build_sumo_edges +from city2graph.networks import _build_sumo_nodes +from city2graph.networks import _network_gdfs_to_nx +from city2graph.networks import _parse_sumo_location +from city2graph.networks import _parse_sumo_shape +from city2graph.networks import _safe_int +from city2graph.networks import _sumo_xy_to_lonlat +from city2graph.networks import load_sumo_network + +TESTS_DIR = Path(__file__).parent +DATA_DIR = TESTS_DIR / "data" +SAMPLE_OSM = DATA_DIR / "sample.osm" +SAMPLE_NET = DATA_DIR / "sample.net.xml" + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _make_nodes_gdf() -> gpd.GeoDataFrame: + return gpd.GeoDataFrame( + {"node_id": ["A", "B"], "junction_type": ["priority", "priority"], + "geometry": [Point(-0.1, 51.5), Point(-0.09, 51.51)]}, + geometry="geometry", + crs="EPSG:4326", + ).set_index("node_id") + + +def _make_edges_gdf() -> gpd.GeoDataFrame: + return gpd.GeoDataFrame( + { + "from_id": ["A", "B"], + "to_id": ["B", "C"], + "edge_id": ["AB", "BC"], + "geometry": [ + LineString([(-0.1, 51.5), (-0.09, 51.51)]), + LineString([(-0.09, 51.51), (-0.08, 51.52)]), + ], + }, + geometry="geometry", + crs="EPSG:4326", + ).set_index(["from_id", "to_id"]) + + +# --------------------------------------------------------------------------- +# Unit tests: helpers +# --------------------------------------------------------------------------- + + +class TestSafeInt: + def test_valid(self) -> None: + assert _safe_int("3") == 3 + + def test_none(self) -> None: + assert _safe_int(None) is None + + def test_invalid(self) -> None: + assert _safe_int("abc") is None + + +class TestParseSumoLocation: + def test_none_element(self) -> None: + ox, oy, proj = _parse_sumo_location(None) + assert ox == 0.0 + assert oy == 0.0 + assert proj is None + + def test_no_projection(self) -> None: + elem = ET.fromstring( + '' + ) + ox, oy, proj = _parse_sumo_location(elem) + assert ox == 10.0 + assert oy == 20.0 + assert proj is None + + def test_with_projection(self) -> None: + elem = ET.fromstring( + '' + ) + _, _, proj = _parse_sumo_location(elem) + assert proj is not None + assert "+proj=utm" in proj + + def test_malformed_offset_defaults(self) -> None: + elem = ET.fromstring('') + ox, oy, proj = _parse_sumo_location(elem) + assert ox == 0.0 + assert oy == 0.0 + + +class TestSumoXyToLonLat: + def test_no_projection_passthrough(self) -> None: + lon, lat = _sumo_xy_to_lonlat( + -0.1, 51.5, offset_x=0.0, offset_y=0.0, proj_string=None + ) + assert lon == pytest.approx(-0.1) + assert lat == pytest.approx(51.5) + + def test_offset_applied(self) -> None: + # sumo coords = projected - offset → projected = sumo + offset + lon, lat = _sumo_xy_to_lonlat( + 5.0, 10.0, offset_x=1.0, offset_y=2.0, proj_string=None + ) + assert lon == pytest.approx(6.0) + assert lat == pytest.approx(12.0) + + def test_invalid_proj_falls_back(self) -> None: + # Should log a warning and return offset-corrected values without raising. + lon, lat = _sumo_xy_to_lonlat( + 0.0, 0.0, offset_x=1.0, offset_y=2.0, proj_string="+proj=invalid_xyz" + ) + assert isinstance(lon, float) + assert isinstance(lat, float) + + +class TestParseSumoShape: + def test_valid_shape(self) -> None: + geom = _parse_sumo_shape( + "-0.1,51.5 -0.09,51.51", + offset_x=0.0, + offset_y=0.0, + proj_string=None, + ) + assert isinstance(geom, LineString) + assert len(geom.coords) == 2 + + def test_empty_shape_returns_none(self) -> None: + geom = _parse_sumo_shape("", offset_x=0.0, offset_y=0.0, proj_string=None) + assert geom is None + + def test_single_point_returns_none(self) -> None: + geom = _parse_sumo_shape( + "-0.1,51.5", offset_x=0.0, offset_y=0.0, proj_string=None + ) + assert geom is None + + def test_malformed_pair_skipped(self) -> None: + # One valid pair + one bad → None (fewer than 2 coords) + geom = _parse_sumo_shape( + "-0.1,51.5 bad", offset_x=0.0, offset_y=0.0, proj_string=None + ) + assert geom is None + + +class TestNetworkGdfsToNx: + def test_directed(self) -> None: + G = _network_gdfs_to_nx(_make_nodes_gdf(), _make_edges_gdf(), directed=True) + assert isinstance(G, nx.DiGraph) + # A and B come from nodes_gdf; C is added implicitly by the (B, C) edge + assert {"A", "B"}.issubset(set(G.nodes)) + assert ("A", "B") in G.edges + + def test_undirected(self) -> None: + G = _network_gdfs_to_nx(_make_nodes_gdf(), _make_edges_gdf(), directed=False) + assert isinstance(G, nx.Graph) + assert not isinstance(G, nx.DiGraph) + + def test_crs_stored(self) -> None: + G = _network_gdfs_to_nx(_make_nodes_gdf(), _make_edges_gdf(), directed=True) + assert "EPSG:4326" in G.graph["crs"] + + def test_node_geometry_attribute(self) -> None: + G = _network_gdfs_to_nx(_make_nodes_gdf(), _make_edges_gdf(), directed=True) + assert isinstance(G.nodes["A"]["geometry"], Point) + + +# --------------------------------------------------------------------------- +# Unit tests: SUMO XML builders +# --------------------------------------------------------------------------- + + +class TestBuildSumoNodes: + def _root(self) -> ET.Element: + return ET.fromstring( + """ + + + + """ + ) + + def test_internal_junctions_excluded(self) -> None: + gdf = _build_sumo_nodes( + self._root(), offset_x=0.0, offset_y=0.0, proj_string=None + ) + assert ":int" not in gdf.index + assert "A" in gdf.index + assert "B" in gdf.index + + def test_geometry_is_point(self) -> None: + gdf = _build_sumo_nodes( + self._root(), offset_x=0.0, offset_y=0.0, proj_string=None + ) + assert isinstance(gdf.loc["A", "geometry"], Point) + + def test_crs(self) -> None: + gdf = _build_sumo_nodes( + self._root(), offset_x=0.0, offset_y=0.0, proj_string=None + ) + assert gdf.crs.to_epsg() == 4326 + + def test_empty_net_returns_empty_gdf(self) -> None: + root = ET.fromstring("") + gdf = _build_sumo_nodes(root, offset_x=0.0, offset_y=0.0, proj_string=None) + assert isinstance(gdf, gpd.GeoDataFrame) + assert len(gdf) == 0 + + +class TestBuildSumoEdges: + def _root_and_nodes(self) -> tuple[ET.Element, gpd.GeoDataFrame]: + xml = """ + + + + + + + + + """ + root = ET.fromstring(xml) + nodes = _build_sumo_nodes(root, offset_x=0.0, offset_y=0.0, proj_string=None) + return root, nodes + + def test_internal_edges_excluded(self) -> None: + root, nodes = self._root_and_nodes() + gdf = _build_sumo_edges( + root, offset_x=0.0, offset_y=0.0, proj_string=None, nodes_gdf=nodes + ) + assert ":internal" not in gdf.get("edge_id", gdf.reset_index().get("edge_id", [])) + + def test_edge_indexed_by_from_to(self) -> None: + root, nodes = self._root_and_nodes() + gdf = _build_sumo_edges( + root, offset_x=0.0, offset_y=0.0, proj_string=None, nodes_gdf=nodes + ) + assert ("A", "B") in gdf.index + + def test_lane_attributes_aggregated(self) -> None: + root, nodes = self._root_and_nodes() + gdf = _build_sumo_edges( + root, offset_x=0.0, offset_y=0.0, proj_string=None, nodes_gdf=nodes + ) + assert gdf.loc[("A", "B"), "speed_ms"] == pytest.approx(13.89) + assert gdf.loc[("A", "B"), "length_m"] == pytest.approx(100.0) + assert gdf.loc[("A", "B"), "num_lanes"] == 1 + + def test_geometry_is_linestring(self) -> None: + root, nodes = self._root_and_nodes() + gdf = _build_sumo_edges( + root, offset_x=0.0, offset_y=0.0, proj_string=None, nodes_gdf=nodes + ) + assert isinstance(gdf.loc[("A", "B"), "geometry"], LineString) + + def test_fallback_straight_line_when_no_shape(self) -> None: + xml = """ + + + + + + """ + root = ET.fromstring(xml) + nodes = _build_sumo_nodes(root, offset_x=0.0, offset_y=0.0, proj_string=None) + gdf = _build_sumo_edges( + root, offset_x=0.0, offset_y=0.0, proj_string=None, nodes_gdf=nodes + ) + assert isinstance(gdf.loc[("A", "B"), "geometry"], LineString) + + +# --------------------------------------------------------------------------- +# Integration tests: load_sumo_network +# --------------------------------------------------------------------------- + + +class TestLoadSumoNetwork: + def test_returns_tuple_by_default(self) -> None: + result = load_sumo_network(SAMPLE_NET) + assert isinstance(result, tuple) + nodes_gdf, edges_gdf = result + assert isinstance(nodes_gdf, gpd.GeoDataFrame) + assert isinstance(edges_gdf, gpd.GeoDataFrame) + + def test_nodes_indexed_by_junction_id(self) -> None: + nodes_gdf, _ = load_sumo_network(SAMPLE_NET) + assert nodes_gdf.index.name == "node_id" + assert "A" in nodes_gdf.index + assert "B" in nodes_gdf.index + assert "C" in nodes_gdf.index + + def test_internal_nodes_excluded(self) -> None: + nodes_gdf, _ = load_sumo_network(SAMPLE_NET) + assert not any(str(idx).startswith(":") for idx in nodes_gdf.index) + + def test_internal_edges_excluded(self) -> None: + _, edges_gdf = load_sumo_network(SAMPLE_NET) + index_tuples = list(edges_gdf.index) + assert not any(str(f).startswith(":") or str(t).startswith(":") for f, t in index_tuples) + + def test_nodes_crs(self) -> None: + nodes_gdf, _ = load_sumo_network(SAMPLE_NET) + assert nodes_gdf.crs.to_epsg() == 4326 + + def test_edges_crs(self) -> None: + _, edges_gdf = load_sumo_network(SAMPLE_NET) + assert edges_gdf.crs.to_epsg() == 4326 + + def test_nodes_geometry_is_point(self) -> None: + nodes_gdf, _ = load_sumo_network(SAMPLE_NET) + assert all(isinstance(g, Point) for g in nodes_gdf.geometry) + + def test_edges_geometry_is_linestring(self) -> None: + _, edges_gdf = load_sumo_network(SAMPLE_NET) + assert all(isinstance(g, LineString) for g in edges_gdf.geometry if g is not None) + + def test_directed_true_preserves_all_edges(self) -> None: + _, edges_gdf = load_sumo_network(SAMPLE_NET, directed=True) + # Fixture has AB, BC, BA → all three should be present + index_tuples = {(f, t) for f, t in edges_gdf.index} + assert ("A", "B") in index_tuples + assert ("B", "A") in index_tuples + + def test_directed_false_deduplicates(self) -> None: + _, edges_gdf = load_sumo_network(SAMPLE_NET, directed=False) + index_tuples = list(edges_gdf.index) + # No pair should appear in both directions + pairs = {(f, t) for f, t in index_tuples} + for f, t in pairs: + assert (t, f) not in pairs or f == t + + def test_as_nx_directed(self) -> None: + G = load_sumo_network(SAMPLE_NET, as_nx=True, directed=True) + assert isinstance(G, nx.DiGraph) + assert G.number_of_nodes() > 0 + + def test_as_nx_undirected(self) -> None: + G = load_sumo_network(SAMPLE_NET, as_nx=True, directed=False) + assert isinstance(G, nx.Graph) + assert not isinstance(G, nx.DiGraph) + + def test_file_not_found(self) -> None: + with pytest.raises(FileNotFoundError): + load_sumo_network("nonexistent.net.xml") + + def test_invalid_xml(self, tmp_path: Path) -> None: + bad_file = tmp_path / "bad.net.xml" + bad_file.write_text("not valid xml <<<") + with pytest.raises(ValueError, match="Failed to parse"): + load_sumo_network(bad_file) + + def test_edge_attributes_present(self) -> None: + _, edges_gdf = load_sumo_network(SAMPLE_NET) + assert "num_lanes" in edges_gdf.columns + assert "speed_ms" in edges_gdf.columns + assert "length_m" in edges_gdf.columns + assert "priority" in edges_gdf.columns + + def test_multi_lane_speed_averaged(self) -> None: + # Fixture edge AB has 2 lanes both at 13.89 m/s + _, edges_gdf = load_sumo_network(SAMPLE_NET, directed=True) + assert edges_gdf.loc[("A", "B"), "speed_ms"] == pytest.approx(13.89) + assert edges_gdf.loc[("A", "B"), "num_lanes"] == 2 + + +# --------------------------------------------------------------------------- +# Integration tests: load_osm_network +# --------------------------------------------------------------------------- + + +class TestLoadOsmNetwork: + @pytest.fixture() + def osm_result(self) -> tuple[gpd.GeoDataFrame, gpd.GeoDataFrame]: + from city2graph.networks import load_osm_network + + return load_osm_network(SAMPLE_OSM) # type: ignore[return-value] + + def test_returns_tuple( + self, osm_result: tuple[gpd.GeoDataFrame, gpd.GeoDataFrame] + ) -> None: + assert isinstance(osm_result, tuple) + assert len(osm_result) == 2 + + def test_nodes_is_geodataframe( + self, osm_result: tuple[gpd.GeoDataFrame, gpd.GeoDataFrame] + ) -> None: + nodes_gdf, _ = osm_result + assert isinstance(nodes_gdf, gpd.GeoDataFrame) + + def test_edges_is_geodataframe( + self, osm_result: tuple[gpd.GeoDataFrame, gpd.GeoDataFrame] + ) -> None: + _, edges_gdf = osm_result + assert isinstance(edges_gdf, gpd.GeoDataFrame) + + def test_nodes_crs( + self, osm_result: tuple[gpd.GeoDataFrame, gpd.GeoDataFrame] + ) -> None: + nodes_gdf, _ = osm_result + assert nodes_gdf.crs.to_epsg() == 4326 + + def test_edges_crs( + self, osm_result: tuple[gpd.GeoDataFrame, gpd.GeoDataFrame] + ) -> None: + _, edges_gdf = osm_result + assert edges_gdf.crs.to_epsg() == 4326 + + def test_nodes_geometry_is_point( + self, osm_result: tuple[gpd.GeoDataFrame, gpd.GeoDataFrame] + ) -> None: + nodes_gdf, _ = osm_result + assert all(isinstance(g, Point) for g in nodes_gdf.geometry) + + def test_edges_index_is_u_v( + self, osm_result: tuple[gpd.GeoDataFrame, gpd.GeoDataFrame] + ) -> None: + _, edges_gdf = osm_result + assert edges_gdf.index.names == ["u", "v"] + + def test_undirected_no_reverse_duplicates(self) -> None: + from city2graph.networks import load_osm_network + + _, edges_gdf = load_osm_network(SAMPLE_OSM, directed=False) + pairs = list(edges_gdf.index) + for u, v in pairs: + assert (v, u) not in pairs or u == v + + def test_as_nx_returns_digraph_when_directed(self) -> None: + from city2graph.networks import load_osm_network + + G = load_osm_network(SAMPLE_OSM, as_nx=True, directed=True) + assert isinstance(G, nx.DiGraph) + + def test_as_nx_returns_graph_when_undirected(self) -> None: + from city2graph.networks import load_osm_network + + G = load_osm_network(SAMPLE_OSM, as_nx=True, directed=False) + assert isinstance(G, nx.Graph) + assert not isinstance(G, nx.DiGraph) + + def test_file_not_found(self) -> None: + from city2graph.networks import load_osm_network + + with pytest.raises(FileNotFoundError): + load_osm_network("nonexistent.osm") diff --git a/uv.lock b/uv.lock index 1f4410c..6dc4ee9 100644 --- a/uv.lock +++ b/uv.lock @@ -559,6 +559,9 @@ cu130 = [ { name = "torch-geometric" }, { name = "torchvision" }, ] +sumo = [ + { name = "sumolib" }, +] [package.dev-dependencies] dev = [ @@ -618,6 +621,7 @@ requires-dist = [ { name = "rustworkx", specifier = ">=0.17.1" }, { name = "scipy", specifier = ">=1.10.0" }, { name = "shapely", specifier = ">=2.1.0" }, + { name = "sumolib", marker = "extra == 'sumo'", specifier = ">=1.20.0" }, { name = "torch", marker = "(sys_platform == 'linux' and extra == 'cu126') or (sys_platform == 'win32' and extra == 'cu126')", specifier = ">=2.8.0", index = "https://download.pytorch.org/whl/cu126", conflict = { package = "city2graph", extra = "cu126" } }, { name = "torch", marker = "(sys_platform == 'linux' and extra == 'cu128') or (sys_platform == 'win32' and extra == 'cu128')", specifier = ">=2.9.0", index = "https://download.pytorch.org/whl/cu128", conflict = { package = "city2graph", extra = "cu128" } }, { name = "torch", marker = "(sys_platform == 'linux' and extra == 'cu130') or (sys_platform == 'win32' and extra == 'cu130')", specifier = ">=2.9.0", index = "https://download.pytorch.org/whl/cu130", conflict = { package = "city2graph", extra = "cu130" } }, @@ -634,7 +638,7 @@ requires-dist = [ { name = "torchvision", marker = "extra == 'cu128'", specifier = ">=0.24.0" }, { name = "torchvision", marker = "extra == 'cu130'", specifier = ">=0.24.0" }, ] -provides-extras = ["cpu", "cu126", "cu128", "cu130"] +provides-extras = ["sumo", "cpu", "cu126", "cu128", "cu130"] [package.metadata.requires-dev] dev = [ @@ -4956,6 +4960,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, ] +[[package]] +name = "sumolib" +version = "1.26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/03/36bbd7b9f6120e39d9e4f6865186ab8195f7b5a3833c37fc8c24df1d23fe/sumolib-1.26.0.tar.gz", hash = "sha256:eedf9cbcea46ac9db6a2a46ee74f85edbf4384de1f6b73bf73550642ba6044c1", size = 138920, upload-time = "2026-01-29T14:54:37.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/2d/44832bb6dec541d7d0d79ce6cf50f4213dcb862f4a7e192c9828841f7d84/sumolib-1.26.0-py2.py3-none-any.whl", hash = "sha256:189011b50db451b44f0ffa3a49e7cefd6250573f0e239fba540e1883ad910eaf", size = 233486, upload-time = "2026-01-29T14:54:35.577Z" }, +] + [[package]] name = "sympy" version = "1.14.0" @@ -5066,16 +5079,16 @@ dependencies = [ { name = "typing-extensions", marker = "(sys_platform == 'darwin' and extra == 'extra-10-city2graph-cpu') or (extra == 'extra-10-city2graph-cpu' and extra == 'extra-10-city2graph-cu126') or (extra == 'extra-10-city2graph-cpu' and extra == 'extra-10-city2graph-cu128') or (extra == 'extra-10-city2graph-cpu' and extra == 'extra-10-city2graph-cu130') or (extra == 'extra-10-city2graph-cu126' and extra == 'extra-10-city2graph-cu128') or (extra == 'extra-10-city2graph-cu126' and extra == 'extra-10-city2graph-cu130') or (extra == 'extra-10-city2graph-cu128' and extra == 'extra-10-city2graph-cu130')" }, ] wheels = [ - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0-1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:0826ac8e409551e12b2360ac18b4161a838cbd111933e694752f351191331d09" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0-1-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:7fbbf409143a4fe0812a40c0b46a436030a7e1d14fe8c5234dfbe44df47f617e" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0-1-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:b39cafff7229699f9d6e172cac74d85fd71b568268e439e08d9c540e54732a3e" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0-2-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:90821a3194b8806d9fa9fdaa9308c1bc73df0c26808274b14129a97c99f35794" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0-2-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:358bd7125cbec6e692d60618a5eec7f55a51b29e3652a849fd42af021d818023" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0-2-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:470de4176007c2700735e003a830828a88d27129032a3add07291da07e2a94e8" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:4584ab167995c0479f6821e3dceaf199c8166c811d3adbba5d8eedbbfa6764fd" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:45a1c5057629444aeb1c452c18298fa7f30f2f7aeadd4dc41f9d340980294407" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:339e05502b6c839db40e88720cb700f5a3b50cda332284873e851772d41b2c1e" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:840351da59cedb7bcbc51981880050813c19ef6b898a7fecf73a3afc71aff3fe" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0-1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:0826ac8e409551e12b2360ac18b4161a838cbd111933e694752f351191331d09", upload-time = "2026-02-06T16:27:14Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0-1-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:7fbbf409143a4fe0812a40c0b46a436030a7e1d14fe8c5234dfbe44df47f617e", upload-time = "2026-02-06T16:27:14Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0-1-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:b39cafff7229699f9d6e172cac74d85fd71b568268e439e08d9c540e54732a3e", upload-time = "2026-02-06T16:27:17Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0-2-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:90821a3194b8806d9fa9fdaa9308c1bc73df0c26808274b14129a97c99f35794", upload-time = "2026-02-10T19:55:42Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0-2-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:358bd7125cbec6e692d60618a5eec7f55a51b29e3652a849fd42af021d818023", upload-time = "2026-02-10T19:55:42Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0-2-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:470de4176007c2700735e003a830828a88d27129032a3add07291da07e2a94e8", upload-time = "2026-02-10T19:55:43Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:4584ab167995c0479f6821e3dceaf199c8166c811d3adbba5d8eedbbfa6764fd", upload-time = "2026-01-23T15:09:55Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:45a1c5057629444aeb1c452c18298fa7f30f2f7aeadd4dc41f9d340980294407", upload-time = "2026-01-23T15:09:55Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:339e05502b6c839db40e88720cb700f5a3b50cda332284873e851772d41b2c1e", upload-time = "2026-01-23T15:09:57Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:840351da59cedb7bcbc51981880050813c19ef6b898a7fecf73a3afc71aff3fe", upload-time = "2026-01-23T15:09:59Z" }, ] [[package]] @@ -5143,29 +5156,29 @@ dependencies = [ { name = "typing-extensions", marker = "(sys_platform != 'darwin' and extra == 'extra-10-city2graph-cpu') or (extra == 'extra-10-city2graph-cpu' and extra == 'extra-10-city2graph-cu126') or (extra == 'extra-10-city2graph-cpu' and extra == 'extra-10-city2graph-cu128') or (extra == 'extra-10-city2graph-cpu' and extra == 'extra-10-city2graph-cu130') or (extra == 'extra-10-city2graph-cu126' and extra == 'extra-10-city2graph-cu128') or (extra == 'extra-10-city2graph-cu126' and extra == 'extra-10-city2graph-cu130') or (extra == 'extra-10-city2graph-cu128' and extra == 'extra-10-city2graph-cu130')" }, ] wheels = [ - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp311-cp311-linux_aarch64.whl", hash = "sha256:ce5c113d1f55f8c1f5af05047a24e50d11d293e0cbbb5bf7a75c6c761edd6eaa" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp311-cp311-linux_s390x.whl", hash = "sha256:0e286fcf6ce0cc7b204396c9b4ea0d375f1f0c3e752f68ce3d3aeb265511db8c" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:1cfcb9b1558c6e52dffd0d4effce83b13c5ae5d97338164c372048c21f9cfccb" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:b7cb1ec66cefb90fd7b676eac72cfda3b8d4e4d0cacd7a531963bc2e0a9710ab" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp311-cp311-win_amd64.whl", hash = "sha256:17a09465bab2aab8f0f273410297133d8d8fb6dd84dccbd252ca4a4f3a111847" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp311-cp311-win_arm64.whl", hash = "sha256:c35c0de592941d4944698dbfa87271ab85d3370eca3b694943a2ab307ac34b3f" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp312-cp312-linux_aarch64.whl", hash = "sha256:8de5a36371b775e2d4881ed12cc7f2de400b1ad3d728aa74a281f649f87c9b8c" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp312-cp312-linux_s390x.whl", hash = "sha256:9accc30b56cb6756d4a9d04fcb8ebc0bb68c7d55c1ed31a8657397d316d31596" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:179451716487f8cb09b56459667fa1f5c4c0946c1e75fbeae77cfc40a5768d87" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ee40b8a4b4b2cf0670c6fd4f35a7ef23871af956fecb238fbf5da15a72650b1d" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp312-cp312-win_amd64.whl", hash = "sha256:21cb5436978ef47c823b7a813ff0f8c2892e266cfe0f1d944879b5fba81bf4e1" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp312-cp312-win_arm64.whl", hash = "sha256:3eaa727e6a73affa61564d86b9d03191df45c8650d0666bd3d57c8597ef61e78" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp313-cp313-linux_aarch64.whl", hash = "sha256:fd215f3d0f681905c5b56b0630a3d666900a37fcc3ca5b937f95275c66f9fd9c" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp313-cp313-linux_s390x.whl", hash = "sha256:170a0623108055be5199370335cf9b41ba6875b3cb6f086db4aee583331a4899" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e51994492cdb76edce29da88de3672a3022f9ef0ffd90345436948d4992be2c7" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8d316e5bf121f1eab1147e49ad0511a9d92e4c45cc357d1ab0bee440da71a095" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp313-cp313-win_amd64.whl", hash = "sha256:b719da5af01b59126ac13eefd6ba3dd12d002dc0e8e79b8b365e55267a8189d3" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp313-cp313-win_arm64.whl", hash = "sha256:b67d91326e4ed9eccbd6b7d84ed7ffa43f93103aa3f0b24145f3001f3b11b714" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp313-cp313t-linux_aarch64.whl", hash = "sha256:5af75e5f49de21b0bdf7672bc27139bd285f9e8dbcabe2d617a2eb656514ac36" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp313-cp313t-linux_s390x.whl", hash = "sha256:ba51ef01a510baf8fff576174f702c47e1aa54389a9f1fba323bb1a5003ff0bf" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:0fedcb1a77e8f2aaf7bfd21591bf6d1e0b207473268c9be16b17cb7783253969" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:106dd1930cb30a4a337366ba3f9b25318ebf940f51fd46f789281dd9e736bdc4" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp313-cp313t-win_amd64.whl", hash = "sha256:eb1bde1ce198f05c8770017de27e001d404499cf552aaaa014569eff56ca25c0" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp311-cp311-linux_aarch64.whl", hash = "sha256:ce5c113d1f55f8c1f5af05047a24e50d11d293e0cbbb5bf7a75c6c761edd6eaa", upload-time = "2026-01-23T15:10:11Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp311-cp311-linux_s390x.whl", hash = "sha256:0e286fcf6ce0cc7b204396c9b4ea0d375f1f0c3e752f68ce3d3aeb265511db8c", upload-time = "2026-01-23T15:10:12Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:1cfcb9b1558c6e52dffd0d4effce83b13c5ae5d97338164c372048c21f9cfccb", upload-time = "2026-01-23T15:10:15Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:b7cb1ec66cefb90fd7b676eac72cfda3b8d4e4d0cacd7a531963bc2e0a9710ab", upload-time = "2026-01-23T15:10:15Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp311-cp311-win_amd64.whl", hash = "sha256:17a09465bab2aab8f0f273410297133d8d8fb6dd84dccbd252ca4a4f3a111847", upload-time = "2026-01-23T15:10:19Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp311-cp311-win_arm64.whl", hash = "sha256:c35c0de592941d4944698dbfa87271ab85d3370eca3b694943a2ab307ac34b3f", upload-time = "2026-01-23T15:10:20Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp312-cp312-linux_aarch64.whl", hash = "sha256:8de5a36371b775e2d4881ed12cc7f2de400b1ad3d728aa74a281f649f87c9b8c", upload-time = "2026-01-23T15:10:22Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp312-cp312-linux_s390x.whl", hash = "sha256:9accc30b56cb6756d4a9d04fcb8ebc0bb68c7d55c1ed31a8657397d316d31596", upload-time = "2026-01-23T15:10:24Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:179451716487f8cb09b56459667fa1f5c4c0946c1e75fbeae77cfc40a5768d87", upload-time = "2026-01-23T15:10:25Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ee40b8a4b4b2cf0670c6fd4f35a7ef23871af956fecb238fbf5da15a72650b1d", upload-time = "2026-01-23T15:10:27Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp312-cp312-win_amd64.whl", hash = "sha256:21cb5436978ef47c823b7a813ff0f8c2892e266cfe0f1d944879b5fba81bf4e1", upload-time = "2026-01-23T15:10:30Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp312-cp312-win_arm64.whl", hash = "sha256:3eaa727e6a73affa61564d86b9d03191df45c8650d0666bd3d57c8597ef61e78", upload-time = "2026-01-23T15:10:31Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp313-cp313-linux_aarch64.whl", hash = "sha256:fd215f3d0f681905c5b56b0630a3d666900a37fcc3ca5b937f95275c66f9fd9c", upload-time = "2026-01-23T15:10:34Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp313-cp313-linux_s390x.whl", hash = "sha256:170a0623108055be5199370335cf9b41ba6875b3cb6f086db4aee583331a4899", upload-time = "2026-01-23T15:10:35Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e51994492cdb76edce29da88de3672a3022f9ef0ffd90345436948d4992be2c7", upload-time = "2026-01-23T15:10:37Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8d316e5bf121f1eab1147e49ad0511a9d92e4c45cc357d1ab0bee440da71a095", upload-time = "2026-01-23T15:10:38Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp313-cp313-win_amd64.whl", hash = "sha256:b719da5af01b59126ac13eefd6ba3dd12d002dc0e8e79b8b365e55267a8189d3", upload-time = "2026-01-23T15:10:41Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp313-cp313-win_arm64.whl", hash = "sha256:b67d91326e4ed9eccbd6b7d84ed7ffa43f93103aa3f0b24145f3001f3b11b714", upload-time = "2026-01-23T15:10:42Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp313-cp313t-linux_aarch64.whl", hash = "sha256:5af75e5f49de21b0bdf7672bc27139bd285f9e8dbcabe2d617a2eb656514ac36", upload-time = "2026-01-23T15:10:44Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp313-cp313t-linux_s390x.whl", hash = "sha256:ba51ef01a510baf8fff576174f702c47e1aa54389a9f1fba323bb1a5003ff0bf", upload-time = "2026-01-23T15:10:48Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:0fedcb1a77e8f2aaf7bfd21591bf6d1e0b207473268c9be16b17cb7783253969", upload-time = "2026-01-23T15:10:48Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:106dd1930cb30a4a337366ba3f9b25318ebf940f51fd46f789281dd9e736bdc4", upload-time = "2026-01-23T15:10:50Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.10.0%2Bcpu-cp313-cp313t-win_amd64.whl", hash = "sha256:eb1bde1ce198f05c8770017de27e001d404499cf552aaaa014569eff56ca25c0", upload-time = "2026-01-23T15:10:50Z" }, ] [[package]] @@ -5203,18 +5216,18 @@ dependencies = [ { name = "typing-extensions", marker = "(sys_platform == 'linux' and extra == 'extra-10-city2graph-cu126') or (sys_platform == 'win32' and extra == 'extra-10-city2graph-cu126') or (extra == 'extra-10-city2graph-cu126' and extra == 'extra-10-city2graph-cu128') or (extra == 'extra-10-city2graph-cu126' and extra == 'extra-10-city2graph-cu130') or (extra == 'extra-10-city2graph-cpu' and extra == 'extra-10-city2graph-cu126') or (extra == 'extra-10-city2graph-cpu' and extra == 'extra-10-city2graph-cu128') or (extra == 'extra-10-city2graph-cpu' and extra == 'extra-10-city2graph-cu130') or (extra == 'extra-10-city2graph-cu128' and extra == 'extra-10-city2graph-cu130')" }, ] wheels = [ - { url = "https://download-r2.pytorch.org/whl/cu126/torch-2.10.0%2Bcu126-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:3a5fb967ffb53ffa0d2579c9819491cfc36c557040de6fdeabcfcfb45df019bc" }, - { url = "https://download-r2.pytorch.org/whl/cu126/torch-2.10.0%2Bcu126-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a9a9ba3b2baf23c044499ffbcbed88e04b6e38b94189c7dc42dd2cfcdd8c55c0" }, - { url = "https://download-r2.pytorch.org/whl/cu126/torch-2.10.0%2Bcu126-cp311-cp311-win_amd64.whl", hash = "sha256:4749cd32e32ed55179ff2ff0407e0ae5077fe4d332bfa49258f4578d09eccb40" }, - { url = "https://download-r2.pytorch.org/whl/cu126/torch-2.10.0%2Bcu126-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:81264238b3d8840276dd30c31f393e325b8f5da6390d18ac2a80dacecfd693ea" }, - { url = "https://download-r2.pytorch.org/whl/cu126/torch-2.10.0%2Bcu126-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:2a7a569206f07965eff69b28e147676540bb0ba6e1a39410802b6e4708cb8356" }, - { url = "https://download-r2.pytorch.org/whl/cu126/torch-2.10.0%2Bcu126-cp312-cp312-win_amd64.whl", hash = "sha256:95d8409b8a15191de4c2958e86ca47f3ea8f9739b994ee4ca0e7586f37336413" }, - { url = "https://download-r2.pytorch.org/whl/cu126/torch-2.10.0%2Bcu126-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:9ffbf240bc193841ba0a79976510aa9ec14c95a57699257b581bc782316b592f" }, - { url = "https://download-r2.pytorch.org/whl/cu126/torch-2.10.0%2Bcu126-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8568052253534abe27b3ac56d301f69d35ef5ce16479e6a3d7808fb052310919" }, - { url = "https://download-r2.pytorch.org/whl/cu126/torch-2.10.0%2Bcu126-cp313-cp313-win_amd64.whl", hash = "sha256:91e21e7ad572bf0136e5b7f192714f120c8abde8e128f1a0759f158951643822" }, - { url = "https://download-r2.pytorch.org/whl/cu126/torch-2.10.0%2Bcu126-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c3480edd0ecc95df5f3418687f584037c072392646f94f5181d32bba5446724f" }, - { url = "https://download-r2.pytorch.org/whl/cu126/torch-2.10.0%2Bcu126-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:270918b7a7ae46951fae6150bee9fcbd6a908242a1acc8d7e73de1194a041902" }, - { url = "https://download-r2.pytorch.org/whl/cu126/torch-2.10.0%2Bcu126-cp313-cp313t-win_amd64.whl", hash = "sha256:06335b76cbaae9ee94071e69dd79ecfadab76a48edd4ef79a95de0fbf1bc04b4" }, + { url = "https://download-r2.pytorch.org/whl/cu126/torch-2.10.0%2Bcu126-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:3a5fb967ffb53ffa0d2579c9819491cfc36c557040de6fdeabcfcfb45df019bc", upload-time = "2026-01-21T19:34:08Z" }, + { url = "https://download-r2.pytorch.org/whl/cu126/torch-2.10.0%2Bcu126-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a9a9ba3b2baf23c044499ffbcbed88e04b6e38b94189c7dc42dd2cfcdd8c55c0", upload-time = "2026-01-21T19:34:12Z" }, + { url = "https://download-r2.pytorch.org/whl/cu126/torch-2.10.0%2Bcu126-cp311-cp311-win_amd64.whl", hash = "sha256:4749cd32e32ed55179ff2ff0407e0ae5077fe4d332bfa49258f4578d09eccb40", upload-time = "2026-01-21T19:34:21Z" }, + { url = "https://download-r2.pytorch.org/whl/cu126/torch-2.10.0%2Bcu126-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:81264238b3d8840276dd30c31f393e325b8f5da6390d18ac2a80dacecfd693ea", upload-time = "2026-01-21T19:34:23Z" }, + { url = "https://download-r2.pytorch.org/whl/cu126/torch-2.10.0%2Bcu126-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:2a7a569206f07965eff69b28e147676540bb0ba6e1a39410802b6e4708cb8356", upload-time = "2026-01-21T19:34:39Z" }, + { url = "https://download-r2.pytorch.org/whl/cu126/torch-2.10.0%2Bcu126-cp312-cp312-win_amd64.whl", hash = "sha256:95d8409b8a15191de4c2958e86ca47f3ea8f9739b994ee4ca0e7586f37336413", upload-time = "2026-01-21T19:34:42Z" }, + { url = "https://download-r2.pytorch.org/whl/cu126/torch-2.10.0%2Bcu126-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:9ffbf240bc193841ba0a79976510aa9ec14c95a57699257b581bc782316b592f", upload-time = "2026-01-21T19:34:47Z" }, + { url = "https://download-r2.pytorch.org/whl/cu126/torch-2.10.0%2Bcu126-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8568052253534abe27b3ac56d301f69d35ef5ce16479e6a3d7808fb052310919", upload-time = "2026-01-21T19:35:02Z" }, + { url = "https://download-r2.pytorch.org/whl/cu126/torch-2.10.0%2Bcu126-cp313-cp313-win_amd64.whl", hash = "sha256:91e21e7ad572bf0136e5b7f192714f120c8abde8e128f1a0759f158951643822", upload-time = "2026-01-21T19:35:12Z" }, + { url = "https://download-r2.pytorch.org/whl/cu126/torch-2.10.0%2Bcu126-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c3480edd0ecc95df5f3418687f584037c072392646f94f5181d32bba5446724f", upload-time = "2026-01-21T19:35:37Z" }, + { url = "https://download-r2.pytorch.org/whl/cu126/torch-2.10.0%2Bcu126-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:270918b7a7ae46951fae6150bee9fcbd6a908242a1acc8d7e73de1194a041902", upload-time = "2026-01-21T19:35:48Z" }, + { url = "https://download-r2.pytorch.org/whl/cu126/torch-2.10.0%2Bcu126-cp313-cp313t-win_amd64.whl", hash = "sha256:06335b76cbaae9ee94071e69dd79ecfadab76a48edd4ef79a95de0fbf1bc04b4", upload-time = "2026-01-21T19:35:54Z" }, ] [[package]] @@ -5252,18 +5265,18 @@ dependencies = [ { name = "typing-extensions", marker = "(sys_platform == 'linux' and extra == 'extra-10-city2graph-cu128') or (sys_platform == 'win32' and extra == 'extra-10-city2graph-cu128') or (extra == 'extra-10-city2graph-cu128' and extra == 'extra-10-city2graph-cu130') or (extra == 'extra-10-city2graph-cpu' and extra == 'extra-10-city2graph-cu126') or (extra == 'extra-10-city2graph-cpu' and extra == 'extra-10-city2graph-cu128') or (extra == 'extra-10-city2graph-cpu' and extra == 'extra-10-city2graph-cu130') or (extra == 'extra-10-city2graph-cu126' and extra == 'extra-10-city2graph-cu128') or (extra == 'extra-10-city2graph-cu126' and extra == 'extra-10-city2graph-cu130')" }, ] wheels = [ - { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:85ed7944655ea6fd69377692e9cbfd7bba28d99696ceae79985e7caa99cf0a95" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1d01ffaebf64715c0f507a39463149cb19e596ff702bd4bcf862601f2881dabc" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp311-cp311-win_amd64.whl", hash = "sha256:3523fda6e2cfab2b04ae09b1424681358e508bb3faa11ceb67004113d5e7acad" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:6f09cdf2415516be028ae82e6b985bcfc3eac37bc52ab401142689f6224516ca" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:628e89bd5110ced7debee2a57c69959725b7fbc64eab81a39dd70e46c7e28ba5" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp312-cp312-win_amd64.whl", hash = "sha256:fbde8f6a9ec8c76979a0d14df21c10b9e5cab6f0d106a73ca73e2179bc597cae" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:bdbcc703382f948e951c063448c9406bf38ce66c41dd698d9e2733fcf96c037a" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:7b4bd23ed63de97456fcc81c26fea9f02ee02ce1112111c4dac0d8cfe574b23e" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp313-cp313-win_amd64.whl", hash = "sha256:4d1b0b49c54223c7c04050b49eac141d77b6edbc34aea1dfc74a6fdb661baa8c" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:f1f8b840c64b645a4bc61a393db48effb9c92b2dc26c8373873911f0750d1ea7" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:23f58258012bcf1c349cb22af387e33aadca7f83ea617b080e774eb41e4fe8ff" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp313-cp313t-win_amd64.whl", hash = "sha256:01b216e097b17a5277cfb47c383cdcacf06abeadcb0daca0c76b59e72854c3b6" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:85ed7944655ea6fd69377692e9cbfd7bba28d99696ceae79985e7caa99cf0a95", upload-time = "2026-01-21T15:21:36Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1d01ffaebf64715c0f507a39463149cb19e596ff702bd4bcf862601f2881dabc", upload-time = "2026-01-21T15:21:40Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp311-cp311-win_amd64.whl", hash = "sha256:3523fda6e2cfab2b04ae09b1424681358e508bb3faa11ceb67004113d5e7acad", upload-time = "2026-01-21T15:22:00Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:6f09cdf2415516be028ae82e6b985bcfc3eac37bc52ab401142689f6224516ca", upload-time = "2026-01-21T15:22:03Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:628e89bd5110ced7debee2a57c69959725b7fbc64eab81a39dd70e46c7e28ba5", upload-time = "2026-01-21T15:22:11Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp312-cp312-win_amd64.whl", hash = "sha256:fbde8f6a9ec8c76979a0d14df21c10b9e5cab6f0d106a73ca73e2179bc597cae", upload-time = "2026-01-21T15:22:17Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:bdbcc703382f948e951c063448c9406bf38ce66c41dd698d9e2733fcf96c037a", upload-time = "2026-01-21T15:22:29Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:7b4bd23ed63de97456fcc81c26fea9f02ee02ce1112111c4dac0d8cfe574b23e", upload-time = "2026-01-21T15:22:51Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp313-cp313-win_amd64.whl", hash = "sha256:4d1b0b49c54223c7c04050b49eac141d77b6edbc34aea1dfc74a6fdb661baa8c", upload-time = "2026-01-21T15:22:54Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:f1f8b840c64b645a4bc61a393db48effb9c92b2dc26c8373873911f0750d1ea7", upload-time = "2026-01-21T15:23:28Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:23f58258012bcf1c349cb22af387e33aadca7f83ea617b080e774eb41e4fe8ff", upload-time = "2026-01-21T15:23:31Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp313-cp313t-win_amd64.whl", hash = "sha256:01b216e097b17a5277cfb47c383cdcacf06abeadcb0daca0c76b59e72854c3b6", upload-time = "2026-01-21T15:23:53Z" }, ] [[package]] @@ -5301,18 +5314,18 @@ dependencies = [ { name = "typing-extensions", marker = "(sys_platform == 'linux' and extra == 'extra-10-city2graph-cu130') or (sys_platform == 'win32' and extra == 'extra-10-city2graph-cu130') or (extra == 'extra-10-city2graph-cu128' and extra == 'extra-10-city2graph-cu130') or (extra == 'extra-10-city2graph-cpu' and extra == 'extra-10-city2graph-cu126') or (extra == 'extra-10-city2graph-cpu' and extra == 'extra-10-city2graph-cu128') or (extra == 'extra-10-city2graph-cpu' and extra == 'extra-10-city2graph-cu130') or (extra == 'extra-10-city2graph-cu126' and extra == 'extra-10-city2graph-cu128') or (extra == 'extra-10-city2graph-cu126' and extra == 'extra-10-city2graph-cu130')" }, ] wheels = [ - { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:ea3239d544b2e569a8f47db5c7fa4fd42a2fe96aefb84bb1eda45ce213020fd2" }, - { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:22cfa45e73f1e8c64f4012737987a727d01d152121b93d196b0ca22f39a3f8e3" }, - { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp311-cp311-win_amd64.whl", hash = "sha256:218ae0f323d5ebe8f2770e46cbfb7bbff9af2c8d192d5187878d0964d43c8b71" }, - { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:4fc8f67637f4c92b989a07d80ffe755e79a3510ca02ebf23ce66396fb277c88d" }, - { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:858f0cbcc78d726fea9499eb3464faa98392fa093845a3262209bd226b7844d6" }, - { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp312-cp312-win_amd64.whl", hash = "sha256:224649fa0ab181ec483cc368e3303dda1760e4ba31bea806b88979f855436aaa" }, - { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:75780283308df9fede371eeda01e9607c8862a1803a2f2f31a08a2c0deaed342" }, - { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:7e0d9922e9e91f780b2761a0c5ebac3c15c9740bab042e1b59149afa6d6474eb" }, - { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp313-cp313-win_amd64.whl", hash = "sha256:48af94af745a9dd9b42be81ea15b56aba981666bcfe10394dceca6d9476a50fa" }, - { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:46699da91f0367d8dfa1b606cb0352aaf190b5853f463010e75ff08f15a94e7d" }, - { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:775d1fff07e302fb669d555a5005f781aa460aa80dff7a512e8e6e723f9def83" }, - { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp313-cp313t-win_amd64.whl", hash = "sha256:b38e5b505b015903a51c2b3f12e50a9f152f92fe7e3992e79f504138cf90601d" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:ea3239d544b2e569a8f47db5c7fa4fd42a2fe96aefb84bb1eda45ce213020fd2", upload-time = "2026-01-21T19:01:56Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:22cfa45e73f1e8c64f4012737987a727d01d152121b93d196b0ca22f39a3f8e3", upload-time = "2026-01-21T19:02:52Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp311-cp311-win_amd64.whl", hash = "sha256:218ae0f323d5ebe8f2770e46cbfb7bbff9af2c8d192d5187878d0964d43c8b71", upload-time = "2026-01-21T19:00:26Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:4fc8f67637f4c92b989a07d80ffe755e79a3510ca02ebf23ce66396fb277c88d", upload-time = "2026-01-21T19:01:36Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:858f0cbcc78d726fea9499eb3464faa98392fa093845a3262209bd226b7844d6", upload-time = "2026-01-21T19:02:22Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp312-cp312-win_amd64.whl", hash = "sha256:224649fa0ab181ec483cc368e3303dda1760e4ba31bea806b88979f855436aaa", upload-time = "2026-01-21T19:00:27Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:75780283308df9fede371eeda01e9607c8862a1803a2f2f31a08a2c0deaed342", upload-time = "2026-01-21T19:01:39Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:7e0d9922e9e91f780b2761a0c5ebac3c15c9740bab042e1b59149afa6d6474eb", upload-time = "2026-01-21T19:02:26Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp313-cp313-win_amd64.whl", hash = "sha256:48af94af745a9dd9b42be81ea15b56aba981666bcfe10394dceca6d9476a50fa", upload-time = "2026-01-21T19:00:27Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:46699da91f0367d8dfa1b606cb0352aaf190b5853f463010e75ff08f15a94e7d", upload-time = "2026-01-21T19:02:20Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:775d1fff07e302fb669d555a5005f781aa460aa80dff7a512e8e6e723f9def83", upload-time = "2026-01-21T19:02:42Z" }, + { url = "https://download-r2.pytorch.org/whl/cu130/torch-2.10.0%2Bcu130-cp313-cp313t-win_amd64.whl", hash = "sha256:b38e5b505b015903a51c2b3f12e50a9f152f92fe7e3992e79f504138cf90601d", upload-time = "2026-01-21T19:01:32Z" }, ] [[package]]