mamut-routing-tools 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,13 @@
1
+ # Python tooling outputs
2
+ __pycache__/
3
+ *.py[cod]
4
+ .pytest_cache/
5
+ *.egg-info/
6
+ build/
7
+ .venv/
8
+ .python-version
9
+
10
+ # Local OSM stores and derived caches (workspace data, never tracked)
11
+ /osmdata/
12
+ /graph-cache/
13
+ /workspaces/
@@ -0,0 +1,51 @@
1
+ # This CITATION.cff file was generated with cffinit.
2
+ # Visit https://bit.ly/cffinit to generate yours today!
3
+
4
+ cff-version: 1.2.0
5
+ title: MAMUT-routing-tools
6
+ message: >-
7
+ If you use MAMUT-routing-tools, please cite it using the
8
+ metadata from this file.
9
+ type: software
10
+ authors:
11
+ - given-names: Florian
12
+ family-names: Rascoussier
13
+ alias: Onyr
14
+ affiliation: IMT Atlantique; INSA Lyon
15
+ orcid: "https://orcid.org/0009-0005-3253-9814"
16
+ - given-names: Adrien
17
+ family-names: Pichon
18
+ alias: Anzury
19
+ affiliation: Université Bretagne Sud
20
+ orcid: "https://orcid.org/0009-0005-8630-3962"
21
+ repository-code: "https://github.com/ANR-MAMUT/MAMUT-routing-tools"
22
+ abstract: >-
23
+ MAMUT-routing-tools is the local generation tool suite of the
24
+ MAMUT-routing benchmark project: OpenStreetMap city acquisition,
25
+ a road-graph engine compatible with the project's published
26
+ OSM-derived benchmark data, best-known-solution route-geometry
27
+ materialization, and interactive CVRP/VRPTW instance generation.
28
+ It complements the MAMUT-routing benchmark repository and its
29
+ static website by running all compute-heavy workflows locally,
30
+ supporting reproducible computational experiments in Operations
31
+ Research, Computer Science, and Applied Algorithmics.
32
+ keywords:
33
+ - benchmarking
34
+ - CVRP
35
+ - VRPTW
36
+ - reproducibility
37
+ - vehicle routing problem
38
+ - instance generation
39
+ - best-known solutions
40
+ - BKS
41
+ - open science
42
+ - optimization benchmarks
43
+ - OpenStreetMap
44
+ - OSM
45
+ - road network
46
+ - urban logistics
47
+ - research software
48
+ - routing
49
+ license: MIT
50
+ version: 0.1.0
51
+ date-released: "2026-07-18"
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ANR-MAMUT
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,69 @@
1
+ Metadata-Version: 2.4
2
+ Name: mamut-routing-tools
3
+ Version: 0.1.0
4
+ Summary: Local generation tool suite for MAMUT-routing: OSM acquisition, road-graph engine, route geometry, and CVRP/VRPTW instance generation.
5
+ Project-URL: Repository, https://github.com/ANR-MAMUT/MAMUT-routing-tools
6
+ Project-URL: Website, https://mamut-routing.univ-ubs.fr/
7
+ Author: Adrien Pichon
8
+ Author-email: Florian Rascoussier <onyr.maintainer@gmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: CVRP,OSM,OpenStreetMap,VRP,VRPTW,benchmarking,instance generation,operations research,road network,vehicle routing problem
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Scientific/Engineering
22
+ Requires-Python: >=3.11
23
+ Requires-Dist: httpx>=0.27
24
+ Requires-Dist: mamut-routing-lib
25
+ Requires-Dist: numpy>=1.26
26
+ Requires-Dist: pydantic>=2.7
27
+ Requires-Dist: rustworkx>=0.15
28
+ Requires-Dist: scipy>=1.13
29
+ Requires-Dist: typer>=0.12
30
+ Description-Content-Type: text/markdown
31
+
32
+ # MAMUT-routing-tools
33
+
34
+ Local generation tool suite for the [MAMUT-routing](https://github.com/ANR-MAMUT/MAMUT-routing) benchmark project: OSM city acquisition, a road-graph engine, BKS route-geometry materialization, and interactive CVRP/VRPTW instance generation. The public MAMUT-routing website is fully static; everything compute-heavy lives here and runs on your own machine.
35
+
36
+ Part of the [ANR MAMUT project](https://mamut.roadef.org/).
37
+
38
+ ## Status
39
+
40
+ Beta. The tool suite is being extracted from the website's former Julia backend; interfaces may change between releases.
41
+
42
+ ## Components
43
+
44
+ - `mamut-tools roadgraph`: build and inspect drivable road graphs from OSM XML extracts. The construction is a faithful Python port of the OpenStreetMapX.jl pipeline the project previously used (same road classes, oneway rules, intersection segmentation, ENU distances, and strongly-connected trim), so graphs and route geometry stay consistent with previously published data.
45
+ - `mamut-tools geometry`: materialize road-following polylines for best-known solutions, in the exact artifact format the MAMUT-routing website consumes.
46
+ - Planned: OSM city fetch (Nominatim + Overpass), interactive CVRP/VRPTW generation with a local workbench GUI, and the official time-dependent benchmark campaign pipeline.
47
+
48
+ ## Install
49
+
50
+ Requires Python >= 3.11 and [uv](https://github.com/astral-sh/uv).
51
+
52
+ ```bash
53
+ git clone --recurse-submodules https://github.com/ANR-MAMUT/MAMUT-routing-tools.git
54
+ cd MAMUT-routing-tools
55
+ uv sync
56
+ uv run mamut-tools --help
57
+ ```
58
+
59
+ The `MAMUT-routing-lib` contract library is vendored as a git submodule and installed as an editable dependency.
60
+
61
+ ## Quick examples
62
+
63
+ ```bash
64
+ # Road-graph statistics for a city extract
65
+ uv run mamut-tools roadgraph info path/to/City.osm
66
+
67
+ # Materialize a route-geometry group plan (website build contract)
68
+ uv run mamut-tools geometry materialize-plan plan.json --repo-root path/to/MAMUT-routing --result-dir out/
69
+ ```
@@ -0,0 +1,38 @@
1
+ # MAMUT-routing-tools
2
+
3
+ Local generation tool suite for the [MAMUT-routing](https://github.com/ANR-MAMUT/MAMUT-routing) benchmark project: OSM city acquisition, a road-graph engine, BKS route-geometry materialization, and interactive CVRP/VRPTW instance generation. The public MAMUT-routing website is fully static; everything compute-heavy lives here and runs on your own machine.
4
+
5
+ Part of the [ANR MAMUT project](https://mamut.roadef.org/).
6
+
7
+ ## Status
8
+
9
+ Beta. The tool suite is being extracted from the website's former Julia backend; interfaces may change between releases.
10
+
11
+ ## Components
12
+
13
+ - `mamut-tools roadgraph`: build and inspect drivable road graphs from OSM XML extracts. The construction is a faithful Python port of the OpenStreetMapX.jl pipeline the project previously used (same road classes, oneway rules, intersection segmentation, ENU distances, and strongly-connected trim), so graphs and route geometry stay consistent with previously published data.
14
+ - `mamut-tools geometry`: materialize road-following polylines for best-known solutions, in the exact artifact format the MAMUT-routing website consumes.
15
+ - Planned: OSM city fetch (Nominatim + Overpass), interactive CVRP/VRPTW generation with a local workbench GUI, and the official time-dependent benchmark campaign pipeline.
16
+
17
+ ## Install
18
+
19
+ Requires Python >= 3.11 and [uv](https://github.com/astral-sh/uv).
20
+
21
+ ```bash
22
+ git clone --recurse-submodules https://github.com/ANR-MAMUT/MAMUT-routing-tools.git
23
+ cd MAMUT-routing-tools
24
+ uv sync
25
+ uv run mamut-tools --help
26
+ ```
27
+
28
+ The `MAMUT-routing-lib` contract library is vendored as a git submodule and installed as an editable dependency.
29
+
30
+ ## Quick examples
31
+
32
+ ```bash
33
+ # Road-graph statistics for a city extract
34
+ uv run mamut-tools roadgraph info path/to/City.osm
35
+
36
+ # Materialize a route-geometry group plan (website build contract)
37
+ uv run mamut-tools geometry materialize-plan plan.json --repo-root path/to/MAMUT-routing --result-dir out/
38
+ ```
@@ -0,0 +1,79 @@
1
+ [project]
2
+ name = "mamut-routing-tools"
3
+ version = "0.1.0"
4
+ description = "Local generation tool suite for MAMUT-routing: OSM acquisition, road-graph engine, route geometry, and CVRP/VRPTW instance generation."
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Florian Rascoussier", email = "onyr.maintainer@gmail.com" },
8
+ { name = "Adrien Pichon" },
9
+ ]
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.11"
13
+ keywords = [
14
+ "vehicle routing problem",
15
+ "VRP",
16
+ "CVRP",
17
+ "VRPTW",
18
+ "benchmarking",
19
+ "instance generation",
20
+ "OpenStreetMap",
21
+ "OSM",
22
+ "road network",
23
+ "operations research",
24
+ ]
25
+ classifiers = [
26
+ "Development Status :: 3 - Alpha",
27
+ "Intended Audience :: Science/Research",
28
+ "License :: OSI Approved :: MIT License",
29
+ "Operating System :: OS Independent",
30
+ "Programming Language :: Python",
31
+ "Programming Language :: Python :: 3",
32
+ "Programming Language :: Python :: 3.11",
33
+ "Programming Language :: Python :: 3.12",
34
+ "Programming Language :: Python :: 3.13",
35
+ "Topic :: Scientific/Engineering",
36
+ ]
37
+ dependencies = [
38
+ "mamut-routing-lib",
39
+ "typer>=0.12",
40
+ "pydantic>=2.7",
41
+ "numpy>=1.26",
42
+ "scipy>=1.13",
43
+ "rustworkx>=0.15",
44
+ "httpx>=0.27",
45
+ ]
46
+
47
+ [project.urls]
48
+ Repository = "https://github.com/ANR-MAMUT/MAMUT-routing-tools"
49
+ Website = "https://mamut-routing.univ-ubs.fr/"
50
+
51
+ [project.scripts]
52
+ mamut-tools = "mamut_routing_tools.cli:app"
53
+
54
+ [dependency-groups]
55
+ dev = [
56
+ "pytest>=8.0",
57
+ ]
58
+
59
+ [build-system]
60
+ requires = ["hatchling"]
61
+ build-backend = "hatchling.build"
62
+
63
+ [tool.hatch.build.targets.wheel]
64
+ packages = ["src/mamut_routing_tools"]
65
+
66
+ [tool.hatch.build.targets.sdist]
67
+ exclude = [
68
+ ".git",
69
+ ".gitignore",
70
+ ".gitmodules",
71
+ ".github",
72
+ "MAMUT-routing-lib",
73
+ "nix-dev",
74
+ "tests",
75
+ "uv.lock",
76
+ ]
77
+
78
+ [tool.pytest.ini_options]
79
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ """MAMUT-routing-tools: local OSM acquisition, road-graph engine, route
2
+ geometry, and instance generation for the MAMUT-routing benchmark project."""
3
+
4
+ __version__ = "0.1.0"
@@ -0,0 +1,103 @@
1
+ """mamut-tools: command-line interface of MAMUT-routing-tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Annotated, Optional
8
+
9
+ import typer
10
+
11
+ app = typer.Typer(
12
+ name="mamut-tools",
13
+ help="Local MAMUT-routing tool suite: OSM acquisition, road-graph engine, route geometry, and instance generation.",
14
+ no_args_is_help=True,
15
+ add_completion=False,
16
+ )
17
+
18
+ roadgraph_app = typer.Typer(help="Road-graph engine (OpenStreetMapX-compatible construction).", no_args_is_help=True)
19
+ geometry_app = typer.Typer(help="BKS route-geometry materialization.", no_args_is_help=True)
20
+ osm_app = typer.Typer(help="OSM city acquisition (Nominatim + Overpass).", no_args_is_help=True)
21
+ app.add_typer(roadgraph_app, name="roadgraph")
22
+ app.add_typer(geometry_app, name="geometry")
23
+ app.add_typer(osm_app, name="osm")
24
+
25
+
26
+ @osm_app.command("fetch-city")
27
+ def osm_fetch_city_cmd(
28
+ city: Annotated[str, typer.Argument(help="City or locality name to geocode and download.")],
29
+ country: Annotated[str, typer.Option("--country", help="Optional country to disambiguate the geocode.")] = "",
30
+ osm_dir: Annotated[Path, typer.Option("--osm-dir", help="Directory for the downloaded <city>.osm extract.")] = Path("osmdata"),
31
+ padding_km: Annotated[float, typer.Option("--padding-km", help="Extra bbox padding in km.")] = 0.0,
32
+ max_radius_km: Annotated[float, typer.Option("--max-radius-km", help="Clamp the administrative bbox to a square of this radius around the place's geocode point (0 = no clamp).")] = 0.0,
33
+ ) -> None:
34
+ """Download an OSM extract (roads + amenities) for a city by name."""
35
+ from mamut_routing_tools.osm import fetch_and_store_city_osm
36
+
37
+ summary = fetch_and_store_city_osm(
38
+ city,
39
+ country=country,
40
+ osm_dir=osm_dir,
41
+ padding_km=padding_km,
42
+ max_radius_km=max_radius_km,
43
+ )
44
+ typer.echo(json.dumps(summary, indent=1))
45
+
46
+
47
+ @roadgraph_app.command("info")
48
+ def roadgraph_info_cmd(
49
+ osm_path: Annotated[Path, typer.Argument(help="OSM XML extract to build the road graph from.")],
50
+ only_intersections: Annotated[bool, typer.Option("--only-intersections/--all-nodes")] = True,
51
+ trim_to_connected: Annotated[bool, typer.Option("--trim/--no-trim")] = True,
52
+ ) -> None:
53
+ """Build the road graph and print vertex/edge statistics."""
54
+ from mamut_routing_tools.roadgraph import load_road_graph
55
+
56
+ graph = load_road_graph(
57
+ osm_path,
58
+ only_intersections=only_intersections,
59
+ trim_to_connected=trim_to_connected,
60
+ )
61
+ class_counts: dict[int, int] = {}
62
+ for cls in graph.edge_class:
63
+ class_counts[cls] = class_counts.get(cls, 0) + 1
64
+ typer.echo(
65
+ json.dumps(
66
+ {
67
+ "osm_path": str(graph.osm_path),
68
+ "only_intersections": graph.only_intersections,
69
+ "trim_to_connected": graph.trim_to_connected,
70
+ "vertices": graph.vertex_count,
71
+ "edges": graph.edge_count,
72
+ "total_edge_length_km": round(sum(graph.edge_weight) / 1000.0, 3),
73
+ "edge_class_counts": {str(k): class_counts[k] for k in sorted(class_counts)},
74
+ "ref_lla": {"lat": graph.ref_lla.lat, "lon": graph.ref_lla.lon},
75
+ },
76
+ indent=1,
77
+ )
78
+ )
79
+
80
+
81
+ @geometry_app.command("materialize-plan")
82
+ def geometry_materialize_plan_cmd(
83
+ plan_path: Annotated[Path, typer.Argument(help="Group plan JSON (route_geometry.py contract).")],
84
+ repo_root: Annotated[Path, typer.Option("--repo-root", help="MAMUT-routing repo root the plan paths are relative to.")],
85
+ result_dir: Annotated[Optional[Path], typer.Option("--result-dir", help="Directory for per-group result files. Prints to stdout when omitted.")] = None,
86
+ ) -> None:
87
+ """Materialize a route-geometry group plan (website build contract)."""
88
+ from mamut_routing_tools.geometry import materialize_plan
89
+
90
+ plan = json.loads(plan_path.read_text(encoding="utf-8"))
91
+ results = materialize_plan(repo_root, plan)
92
+ if result_dir is None:
93
+ typer.echo(json.dumps(results, sort_keys=True))
94
+ return
95
+ result_dir.mkdir(parents=True, exist_ok=True)
96
+ for result_file, payload in results.items():
97
+ target = result_dir / result_file
98
+ target.write_text(json.dumps(payload, sort_keys=True, separators=(",", ":")), encoding="utf-8")
99
+ typer.echo(str(target))
100
+
101
+
102
+ if __name__ == "__main__": # pragma: no cover
103
+ app()
@@ -0,0 +1,110 @@
1
+ """Geodetic conversions matching OpenStreetMapX.jl exactly.
2
+
3
+ The Julia pipeline stores road nodes as ENU coordinates linearized around the
4
+ center of the OSM bounds, computes edge lengths as 3D Euclidean distances in
5
+ that frame, and converts back to LLA for polylines. The Python engine keeps
6
+ the same math so graph weights and rendered coordinates agree to floating
7
+ point noise rather than to a model difference.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import math
13
+ from typing import NamedTuple
14
+
15
+ WGS84_A = 6378137.0
16
+ WGS84_B = 6356752.31424518
17
+ WGS84_E2 = 1.0 - (WGS84_B * WGS84_B) / (WGS84_A * WGS84_A)
18
+ WGS84_EP2 = (WGS84_A * WGS84_A) / (WGS84_B * WGS84_B) - 1.0
19
+
20
+
21
+ class LLA(NamedTuple):
22
+ lat: float
23
+ lon: float
24
+ alt: float = 0.0
25
+
26
+
27
+ class ECEF(NamedTuple):
28
+ x: float
29
+ y: float
30
+ z: float
31
+
32
+
33
+ class ENU(NamedTuple):
34
+ east: float
35
+ north: float
36
+ up: float
37
+
38
+
39
+ def ecef_from_lla(lla: LLA) -> ECEF:
40
+ lat_rad = math.radians(lla.lat)
41
+ lon_rad = math.radians(lla.lon)
42
+ sin_lat, cos_lat = math.sin(lat_rad), math.cos(lat_rad)
43
+ sin_lon, cos_lon = math.sin(lon_rad), math.cos(lon_rad)
44
+ n = WGS84_A / math.sqrt(1.0 - WGS84_E2 * sin_lat * sin_lat)
45
+ return ECEF(
46
+ (n + lla.alt) * cos_lat * cos_lon,
47
+ (n + lla.alt) * cos_lat * sin_lon,
48
+ (n * (1.0 - WGS84_E2) + lla.alt) * sin_lat,
49
+ )
50
+
51
+
52
+ def lla_from_ecef(ecef: ECEF) -> LLA:
53
+ x, y, z = ecef
54
+ p = math.hypot(x, y)
55
+ theta = math.atan2(z * WGS84_A, p * WGS84_B)
56
+ lon = math.atan2(y, x)
57
+ lat = math.atan2(
58
+ z + WGS84_EP2 * WGS84_B * math.sin(theta) ** 3,
59
+ p - WGS84_E2 * WGS84_A * math.cos(theta) ** 3,
60
+ )
61
+ n = WGS84_A / math.sqrt(1.0 - WGS84_E2 * math.sin(lat) ** 2)
62
+ alt = p / math.cos(lat) - n
63
+ return LLA(math.degrees(lat), math.degrees(lon), alt)
64
+
65
+
66
+ def enu_from_lla(lla: LLA, ref: LLA) -> ENU:
67
+ ecef = ecef_from_lla(lla)
68
+ ecef_ref = ecef_from_lla(ref)
69
+ dx, dy, dz = ecef.x - ecef_ref.x, ecef.y - ecef_ref.y, ecef.z - ecef_ref.z
70
+ lat_rad = math.radians(ref.lat)
71
+ lon_rad = math.radians(ref.lon)
72
+ sin_lat, cos_lat = math.sin(lat_rad), math.cos(lat_rad)
73
+ sin_lon, cos_lon = math.sin(lon_rad), math.cos(lon_rad)
74
+ east = -sin_lon * dx + cos_lon * dy
75
+ north = -cos_lon * sin_lat * dx - sin_lon * sin_lat * dy + cos_lat * dz
76
+ up = cos_lon * cos_lat * dx + sin_lon * cos_lat * dy + sin_lat * dz
77
+ return ENU(east, north, up)
78
+
79
+
80
+ def lla_from_enu(enu: ENU, ref: LLA) -> LLA:
81
+ lat_rad = math.radians(ref.lat)
82
+ lon_rad = math.radians(ref.lon)
83
+ sin_lat, cos_lat = math.sin(lat_rad), math.cos(lat_rad)
84
+ sin_lon, cos_lon = math.sin(lon_rad), math.cos(lon_rad)
85
+ east, north, up = enu
86
+ dx = -sin_lon * east - cos_lon * sin_lat * north + cos_lon * cos_lat * up
87
+ dy = cos_lon * east - sin_lon * sin_lat * north + sin_lon * cos_lat * up
88
+ dz = cos_lat * north + sin_lat * up
89
+ ecef_ref = ecef_from_lla(ref)
90
+ return lla_from_ecef(ECEF(ecef_ref.x + dx, ecef_ref.y + dy, ecef_ref.z + dz))
91
+
92
+
93
+ def enu_distance(a: ENU, b: ENU) -> float:
94
+ return math.sqrt((b.east - a.east) ** 2 + (b.north - a.north) ** 2 + (b.up - a.up) ** 2)
95
+
96
+
97
+ def bounds_center(min_lat: float, min_lon: float, max_lat: float, max_lon: float) -> LLA:
98
+ lon_mid = (min_lon + max_lon) / 2.0
99
+ lat_mid = (min_lat + max_lat) / 2.0
100
+ if min_lon > max_lon:
101
+ lon_mid = lon_mid - 180.0 if lon_mid > 0 else lon_mid + 180.0
102
+ return LLA(lat_mid, lon_mid)
103
+
104
+
105
+ def haversine_m(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
106
+ r = 6371000.0
107
+ dlat = math.radians(lat2 - lat1)
108
+ dlon = math.radians(lon2 - lon1)
109
+ a = math.sin(dlat / 2.0) ** 2 + math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) * math.sin(dlon / 2.0) ** 2
110
+ return 2.0 * r * math.asin(math.sqrt(a))
@@ -0,0 +1,3 @@
1
+ from mamut_routing_tools.geometry.materialize import materialize_plan
2
+
3
+ __all__ = ["materialize_plan"]