bloomr 0.1.1__py3-none-any.whl
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.
- bloomr/__init__.py +51 -0
- bloomr/bin/bloomr-rust-linux-x86_64 +0 -0
- bloomr/bin/bloomr-rust-macos-aarch64 +0 -0
- bloomr/bin/bloomr-rust-macos-x86_64 +0 -0
- bloomr/bin/bloomr-rust-windows-x86_64.exe +0 -0
- bloomr/download.py +120 -0
- bloomr/solver.py +375 -0
- bloomr/visualize.py +239 -0
- bloomr-0.1.1.dist-info/METADATA +187 -0
- bloomr-0.1.1.dist-info/RECORD +13 -0
- bloomr-0.1.1.dist-info/WHEEL +5 -0
- bloomr-0.1.1.dist-info/licenses/LICENSE +201 -0
- bloomr-0.1.1.dist-info/top_level.txt +1 -0
bloomr/__init__.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Bloomr: Memory-efficient Chinese Postman Problem solver.
|
|
3
|
+
|
|
4
|
+
Bloomr is a modern Python package for solving the Chinese Postman Problem (CPP)
|
|
5
|
+
on road networks. It uses a high-performance Rust backend with the blossom algorithm
|
|
6
|
+
for efficient perfect matching.
|
|
7
|
+
|
|
8
|
+
The package provides a simple, pythonic API for:
|
|
9
|
+
- Downloading road networks from OpenStreetMap via OSMnx
|
|
10
|
+
- Computing optimal routes that traverse every road segment at least once
|
|
11
|
+
- Generating GPS-compatible GPX files for navigation
|
|
12
|
+
|
|
13
|
+
Basic Usage:
|
|
14
|
+
>>> from bloomr import solve_cpp
|
|
15
|
+
>>>
|
|
16
|
+
>>> # Solve CPP for a region
|
|
17
|
+
>>> result = solve_cpp("Jersey, Channel Islands")
|
|
18
|
+
>>> print(result.summary())
|
|
19
|
+
>>>
|
|
20
|
+
>>> # Access the GPX route file
|
|
21
|
+
>>> print(f"GPX route: {result.gpx_path}")
|
|
22
|
+
|
|
23
|
+
Advanced Usage:
|
|
24
|
+
>>> from bloomr import download_graph, solve_cpp
|
|
25
|
+
>>>
|
|
26
|
+
>>> # Download and cache a graph
|
|
27
|
+
>>> graph_path = download_graph("San Francisco, California")
|
|
28
|
+
>>>
|
|
29
|
+
>>> # Solve using the cached graph
|
|
30
|
+
>>> result = solve_cpp(graphml_path=graph_path)
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from .download import download_graph, sanitize_filename
|
|
34
|
+
from .solver import CPPResult, solve_cpp
|
|
35
|
+
|
|
36
|
+
__version__ = "0.1.1"
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"solve_cpp",
|
|
40
|
+
"download_graph",
|
|
41
|
+
"sanitize_filename",
|
|
42
|
+
"CPPResult",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
# Visualization is optional - only import if dependencies are available
|
|
46
|
+
try:
|
|
47
|
+
from .visualize import plot_route_map # noqa: F401
|
|
48
|
+
|
|
49
|
+
__all__.append("plot_route_map")
|
|
50
|
+
except ImportError:
|
|
51
|
+
pass
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
bloomr/download.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Graph downloading functionality using OSMnx.
|
|
3
|
+
|
|
4
|
+
This module provides functions to download road network data from OpenStreetMap
|
|
5
|
+
and save it as GraphML files for use with the Bloomr CPP solver.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional, Union
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
import osmnx as ox
|
|
13
|
+
except ImportError as e:
|
|
14
|
+
raise ImportError(
|
|
15
|
+
"OSMnx is required for downloading graphs. " "Install with: pip install bloomr"
|
|
16
|
+
) from e
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def sanitize_filename(name: str) -> str:
|
|
20
|
+
"""
|
|
21
|
+
Convert a place name to a valid filename.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
name: Place name (e.g., "Jersey, Channel Islands")
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Sanitized filename (e.g., "jersey_channel_islands")
|
|
28
|
+
"""
|
|
29
|
+
return name.lower().replace(" ", "_").replace(",", "").replace("/", "_")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def download_graph(
|
|
33
|
+
place: str,
|
|
34
|
+
*,
|
|
35
|
+
output_dir: Optional[Union[Path, str]] = None,
|
|
36
|
+
network_type: str = "drive_service",
|
|
37
|
+
simplify: bool = False,
|
|
38
|
+
force: bool = False,
|
|
39
|
+
) -> Path:
|
|
40
|
+
"""
|
|
41
|
+
Download road network from OpenStreetMap and save as GraphML.
|
|
42
|
+
|
|
43
|
+
This function downloads a road network for a specified place using OSMnx,
|
|
44
|
+
adds edge speeds and travel times, and saves it as a GraphML file that
|
|
45
|
+
can be used with the Bloomr CPP solver.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
place: OSMnx place query (e.g., "Jersey, Channel Islands",
|
|
49
|
+
"San Francisco, California")
|
|
50
|
+
output_dir: Output directory for GraphML file. Defaults to "graphml_data"
|
|
51
|
+
network_type: Type of network to download. Options:
|
|
52
|
+
- "drive": Car-navigable roads
|
|
53
|
+
- "drive_service": Drive + service roads (default)
|
|
54
|
+
- "walk": Walkable paths
|
|
55
|
+
- "bike": Bikeable paths
|
|
56
|
+
- "all": All streets and paths
|
|
57
|
+
simplify: Whether to simplify the graph by merging nodes and removing
|
|
58
|
+
interstitial nodes. Default False.
|
|
59
|
+
force: Force re-download even if file exists. Default False.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Path to the saved GraphML file
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
ValueError: If the place cannot be found or network_type is invalid
|
|
66
|
+
RuntimeError: If the download or save operation fails
|
|
67
|
+
|
|
68
|
+
Example:
|
|
69
|
+
>>> from bloomr import download_graph
|
|
70
|
+
>>> graph_path = download_graph("Jersey, Channel Islands")
|
|
71
|
+
>>> print(graph_path)
|
|
72
|
+
graphml_data/jersey_channel_islands.graphml
|
|
73
|
+
"""
|
|
74
|
+
# Set default output directory
|
|
75
|
+
if output_dir is None:
|
|
76
|
+
output_dir = Path("graphml_data")
|
|
77
|
+
else:
|
|
78
|
+
output_dir = Path(output_dir)
|
|
79
|
+
|
|
80
|
+
# Create output directory
|
|
81
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
|
|
83
|
+
# Generate filename
|
|
84
|
+
filename = sanitize_filename(place)
|
|
85
|
+
graphml_path = output_dir / f"{filename}.graphml"
|
|
86
|
+
|
|
87
|
+
# Check if file exists
|
|
88
|
+
if graphml_path.exists() and not force:
|
|
89
|
+
print(f"GraphML file already exists: {graphml_path}")
|
|
90
|
+
print("Use force=True to re-download")
|
|
91
|
+
return graphml_path
|
|
92
|
+
|
|
93
|
+
# Configure OSMnx
|
|
94
|
+
ox.settings.use_cache = True
|
|
95
|
+
ox.settings.log_console = False
|
|
96
|
+
|
|
97
|
+
print(f"Downloading road network for: {place}")
|
|
98
|
+
print(f"Network type: {network_type}")
|
|
99
|
+
print("This may take several minutes for large regions...")
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
# Download graph from OpenStreetMap
|
|
103
|
+
G = ox.graph_from_place(place, network_type=network_type, simplify=simplify)
|
|
104
|
+
|
|
105
|
+
print(f"Downloaded: {len(G.nodes)} nodes, {len(G.edges)} edges")
|
|
106
|
+
|
|
107
|
+
# Add speeds and travel times to edges
|
|
108
|
+
print("Adding edge speeds and travel times...")
|
|
109
|
+
G = ox.add_edge_speeds(G)
|
|
110
|
+
G = ox.add_edge_travel_times(G)
|
|
111
|
+
|
|
112
|
+
# Save to GraphML format
|
|
113
|
+
print(f"Saving to: {graphml_path}")
|
|
114
|
+
ox.save_graphml(G, filepath=graphml_path)
|
|
115
|
+
|
|
116
|
+
print(f"Successfully saved GraphML file: {graphml_path}")
|
|
117
|
+
return graphml_path
|
|
118
|
+
|
|
119
|
+
except Exception as e:
|
|
120
|
+
raise RuntimeError(f"Failed to download or save graph: {e}") from e
|
bloomr/solver.py
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Chinese Postman Problem solver using Rust blossom algorithm.
|
|
3
|
+
|
|
4
|
+
This module provides the main solver function that interfaces with the Rust
|
|
5
|
+
backend implementation of the blossom algorithm for efficient perfect matching.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import platform
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional, Union
|
|
14
|
+
|
|
15
|
+
from .download import download_graph, sanitize_filename
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _get_platform_suffix() -> str:
|
|
19
|
+
"""
|
|
20
|
+
Get the platform-specific suffix for the Rust binary.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Platform suffix like 'linux-x86_64', 'macos-aarch64', etc.
|
|
24
|
+
|
|
25
|
+
Raises:
|
|
26
|
+
RuntimeError: If the platform is not supported
|
|
27
|
+
"""
|
|
28
|
+
system = platform.system().lower()
|
|
29
|
+
machine = platform.machine().lower()
|
|
30
|
+
|
|
31
|
+
# Normalize machine architecture
|
|
32
|
+
if machine in ("x86_64", "amd64"):
|
|
33
|
+
arch = "x86_64"
|
|
34
|
+
elif machine in ("arm64", "aarch64"):
|
|
35
|
+
arch = "aarch64"
|
|
36
|
+
else:
|
|
37
|
+
raise RuntimeError(f"Unsupported architecture: {machine}")
|
|
38
|
+
|
|
39
|
+
# Map to platform suffixes
|
|
40
|
+
if system == "linux":
|
|
41
|
+
return f"linux-{arch}"
|
|
42
|
+
elif system == "darwin":
|
|
43
|
+
return f"macos-{arch}"
|
|
44
|
+
elif system == "windows":
|
|
45
|
+
return f"windows-{arch}"
|
|
46
|
+
else:
|
|
47
|
+
raise RuntimeError(f"Unsupported platform: {system}")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _find_rust_binary() -> Path:
|
|
51
|
+
"""
|
|
52
|
+
Find the Rust binary in either the packaged location or development location.
|
|
53
|
+
|
|
54
|
+
Tries in order:
|
|
55
|
+
1. Packaged binary in bloomr/bin/ (platform-specific)
|
|
56
|
+
2. Development release build in bloomr-rust/target/release/
|
|
57
|
+
3. Development debug build in bloomr-rust/target/debug/
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Path to the Rust binary
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
RuntimeError: If binary cannot be found
|
|
64
|
+
"""
|
|
65
|
+
package_dir = Path(__file__).parent
|
|
66
|
+
project_root = package_dir.parent
|
|
67
|
+
|
|
68
|
+
# Try packaged binary first
|
|
69
|
+
try:
|
|
70
|
+
platform_suffix = _get_platform_suffix()
|
|
71
|
+
binary_name = f"bloomr-rust-{platform_suffix}"
|
|
72
|
+
if sys.platform == "win32":
|
|
73
|
+
binary_name += ".exe"
|
|
74
|
+
|
|
75
|
+
packaged_binary = package_dir / "bin" / binary_name
|
|
76
|
+
if packaged_binary.exists():
|
|
77
|
+
return packaged_binary
|
|
78
|
+
except RuntimeError:
|
|
79
|
+
# Platform not supported for packaged binary, fall through to dev builds
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
# Try development builds
|
|
83
|
+
binary_name = "bloomr-rust.exe" if sys.platform == "win32" else "bloomr-rust"
|
|
84
|
+
|
|
85
|
+
# Try release build
|
|
86
|
+
dev_release_binary = project_root / "bloomr-rust" / "target" / "release" / binary_name
|
|
87
|
+
if dev_release_binary.exists():
|
|
88
|
+
return dev_release_binary
|
|
89
|
+
|
|
90
|
+
# Try debug build
|
|
91
|
+
dev_debug_binary = project_root / "bloomr-rust" / "target" / "debug" / binary_name
|
|
92
|
+
if dev_debug_binary.exists():
|
|
93
|
+
return dev_debug_binary
|
|
94
|
+
|
|
95
|
+
raise RuntimeError("Rust binary not found in any expected location")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class CPPResult:
|
|
99
|
+
"""
|
|
100
|
+
Results from solving the Chinese Postman Problem.
|
|
101
|
+
|
|
102
|
+
Attributes:
|
|
103
|
+
graphml_path: Path to the input GraphML file
|
|
104
|
+
gpx_path: Path to the generated GPX route file
|
|
105
|
+
metrics_path: Path to the metrics JSON file
|
|
106
|
+
map_path: Path to the route visualization PNG (if generated)
|
|
107
|
+
metrics: Dictionary containing solution metrics
|
|
108
|
+
region: Region name
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def __init__(
|
|
112
|
+
self,
|
|
113
|
+
graphml_path: Path,
|
|
114
|
+
gpx_path: Path,
|
|
115
|
+
metrics_path: Path,
|
|
116
|
+
metrics: dict,
|
|
117
|
+
region: str,
|
|
118
|
+
map_path: Optional[Path] = None,
|
|
119
|
+
):
|
|
120
|
+
self.graphml_path = graphml_path
|
|
121
|
+
self.gpx_path = gpx_path
|
|
122
|
+
self.metrics_path = metrics_path
|
|
123
|
+
self.map_path = map_path
|
|
124
|
+
self.metrics = metrics
|
|
125
|
+
self.region = region
|
|
126
|
+
|
|
127
|
+
def __repr__(self) -> str:
|
|
128
|
+
return (
|
|
129
|
+
f"CPPResult(region='{self.region}', "
|
|
130
|
+
f"distance={self.metrics.get('total_circuit_distance_km', 0):.2f}km, "
|
|
131
|
+
f"efficiency={self.metrics.get('distance_efficiency', 0):.4f})"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def summary(self) -> str:
|
|
135
|
+
"""
|
|
136
|
+
Get a formatted summary of the solution.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Multi-line string with key metrics
|
|
140
|
+
"""
|
|
141
|
+
m = self.metrics
|
|
142
|
+
map_line = f" Map: {self.map_path}\n" if self.map_path else ""
|
|
143
|
+
|
|
144
|
+
unique_segments = m.get("unique_road_segments", 0)
|
|
145
|
+
base_edges = m.get("base_graph_edges", 0)
|
|
146
|
+
circuit_edges = m.get("circuit_length", 0)
|
|
147
|
+
bidirectional = m.get("bidirectional_edge_pairs", 0)
|
|
148
|
+
oneway = m.get("one_way_edges", 0)
|
|
149
|
+
|
|
150
|
+
orig_dist = m.get("total_original_distance_km", 0)
|
|
151
|
+
circuit_dist = m.get("total_circuit_distance_km", 0)
|
|
152
|
+
extra_dist = circuit_dist - orig_dist
|
|
153
|
+
actual_dup = m.get("duplication_ratio", 0)
|
|
154
|
+
|
|
155
|
+
dist_eff = m.get("distance_efficiency", 0)
|
|
156
|
+
time_eff = m.get("time_efficiency", 0)
|
|
157
|
+
deadhead = m.get("deadhead_percentage", 0)
|
|
158
|
+
|
|
159
|
+
return f"""
|
|
160
|
+
Bloomr CPP Solution (Mixed CPP) for {self.region}
|
|
161
|
+
{'='*70}
|
|
162
|
+
|
|
163
|
+
GRAPH STRUCTURE:
|
|
164
|
+
Unique road segments: {unique_segments}
|
|
165
|
+
- Bidirectional (two-way): {bidirectional}
|
|
166
|
+
- One-way: {oneway}
|
|
167
|
+
Base graph edges: {base_edges}
|
|
168
|
+
(bidirectional edges stored in both directions for Mixed CPP)
|
|
169
|
+
|
|
170
|
+
SOLUTION METRICS:
|
|
171
|
+
Duplication ratio: {actual_dup:.4f}
|
|
172
|
+
- How many times each road is traversed on average
|
|
173
|
+
- Ratio: circuit_edges / unique_segments
|
|
174
|
+
- {circuit_edges} / {unique_segments} = {actual_dup:.4f}
|
|
175
|
+
|
|
176
|
+
Total original distance: {orig_dist:.2f} km (all roads once)
|
|
177
|
+
Total circuit distance: {circuit_dist:.2f} km (actual route)
|
|
178
|
+
Extra distance needed: {extra_dist:.2f} km ({deadhead:.1f}%)
|
|
179
|
+
|
|
180
|
+
Distance efficiency: {dist_eff * 100:.1f}%
|
|
181
|
+
Time efficiency: {time_eff * 100:.1f}%
|
|
182
|
+
|
|
183
|
+
OUTPUT FILES:
|
|
184
|
+
GPX route: {self.gpx_path}
|
|
185
|
+
Metrics: {self.metrics_path}
|
|
186
|
+
{map_line}{'='*70}
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def solve_cpp(
|
|
191
|
+
place: Optional[str] = None,
|
|
192
|
+
*,
|
|
193
|
+
graphml_path: Optional[Union[Path, str]] = None,
|
|
194
|
+
output_dir: Optional[Union[Path, str]] = None,
|
|
195
|
+
network_type: str = "drive_service",
|
|
196
|
+
simplify: bool = False,
|
|
197
|
+
method: str = "blossom",
|
|
198
|
+
verbose: bool = False,
|
|
199
|
+
visualize: bool = True,
|
|
200
|
+
) -> CPPResult:
|
|
201
|
+
"""
|
|
202
|
+
Solve the Chinese Postman Problem for a road network.
|
|
203
|
+
|
|
204
|
+
This function finds an optimal route that traverses every road segment at least once,
|
|
205
|
+
using the Rust implementation for efficient perfect matching.
|
|
206
|
+
|
|
207
|
+
You can either:
|
|
208
|
+
1. Provide a place name, and the graph will be downloaded automatically
|
|
209
|
+
2. Provide a path to an existing GraphML file
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
place: OSMnx place query (e.g., "Jersey, Channel Islands").
|
|
213
|
+
If provided, the graph will be downloaded automatically.
|
|
214
|
+
graphml_path: Path to an existing GraphML file from OSMnx.
|
|
215
|
+
If provided, this takes precedence over place.
|
|
216
|
+
output_dir: Output directory for solution files (GPX, metrics).
|
|
217
|
+
Defaults to "solutions/<region_name>"
|
|
218
|
+
network_type: Type of network to download if place is provided.
|
|
219
|
+
Options: "drive", "drive_service", "walk", "bike", "all"
|
|
220
|
+
simplify: Whether to simplify the downloaded graph. Only used if place is provided.
|
|
221
|
+
method: Graph balancing algorithm to use.
|
|
222
|
+
Only "blossom" is supported: Optimal O(n³) algorithm using Edmonds' blossom
|
|
223
|
+
algorithm for minimum weight perfect matching.
|
|
224
|
+
verbose: Enable verbose logging from the Rust solver
|
|
225
|
+
visualize: Whether to generate a map visualization PNG.
|
|
226
|
+
Requires matplotlib and contextily (install with: pip install 'bloomr[viz]')
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
CPPResult object containing paths to output files and solution metrics
|
|
230
|
+
|
|
231
|
+
Raises:
|
|
232
|
+
ValueError: If neither place nor graphml_path is provided, or if both are provided
|
|
233
|
+
FileNotFoundError: If graphml_path is provided but doesn't exist
|
|
234
|
+
RuntimeError: If the Rust solver fails
|
|
235
|
+
|
|
236
|
+
Examples:
|
|
237
|
+
>>> from bloomr import solve_cpp
|
|
238
|
+
>>>
|
|
239
|
+
>>> # Solve for a place (downloads graph automatically)
|
|
240
|
+
>>> result = solve_cpp("Jersey, Channel Islands")
|
|
241
|
+
>>> print(result.summary())
|
|
242
|
+
>>>
|
|
243
|
+
>>> # Solve for an existing GraphML file
|
|
244
|
+
>>> result = solve_cpp(graphml_path="my_graph.graphml")
|
|
245
|
+
>>> print(f"Route saved to: {result.gpx_path}")
|
|
246
|
+
"""
|
|
247
|
+
# Validate inputs
|
|
248
|
+
if place is None and graphml_path is None:
|
|
249
|
+
raise ValueError("Either 'place' or 'graphml_path' must be provided")
|
|
250
|
+
|
|
251
|
+
if place is not None and graphml_path is not None:
|
|
252
|
+
raise ValueError("Provide either 'place' or 'graphml_path', not both")
|
|
253
|
+
|
|
254
|
+
# Validate method
|
|
255
|
+
if method != "blossom":
|
|
256
|
+
raise ValueError(f"Invalid method '{method}'. Only 'blossom' is supported")
|
|
257
|
+
|
|
258
|
+
# Determine region name
|
|
259
|
+
if place is not None:
|
|
260
|
+
region = sanitize_filename(place)
|
|
261
|
+
# Download graph
|
|
262
|
+
print(f"Downloading graph for: {place}")
|
|
263
|
+
graphml_path = download_graph(
|
|
264
|
+
place,
|
|
265
|
+
network_type=network_type,
|
|
266
|
+
simplify=simplify,
|
|
267
|
+
)
|
|
268
|
+
else:
|
|
269
|
+
assert graphml_path is not None # Type narrowing for mypy
|
|
270
|
+
graphml_path = Path(graphml_path)
|
|
271
|
+
if not graphml_path.exists():
|
|
272
|
+
raise FileNotFoundError(f"GraphML file not found: {graphml_path}")
|
|
273
|
+
region = graphml_path.stem
|
|
274
|
+
|
|
275
|
+
# Set default output directory
|
|
276
|
+
if output_dir is None:
|
|
277
|
+
output_dir = Path("solutions") / region
|
|
278
|
+
else:
|
|
279
|
+
output_dir = Path(output_dir)
|
|
280
|
+
|
|
281
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
282
|
+
|
|
283
|
+
# Find Rust binary
|
|
284
|
+
rust_binary = _find_rust_binary()
|
|
285
|
+
|
|
286
|
+
if not rust_binary.exists():
|
|
287
|
+
raise RuntimeError(
|
|
288
|
+
"Rust binary not found. Please ensure bloomr is installed correctly.\n"
|
|
289
|
+
"If you're developing locally, build it first:\n"
|
|
290
|
+
" cd bloomr-rust\n"
|
|
291
|
+
" cargo build --release"
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
# Prepare command
|
|
295
|
+
cmd = [
|
|
296
|
+
str(rust_binary),
|
|
297
|
+
"--graphml",
|
|
298
|
+
str(graphml_path),
|
|
299
|
+
"--output-dir",
|
|
300
|
+
str(output_dir),
|
|
301
|
+
"--region",
|
|
302
|
+
region,
|
|
303
|
+
"--method",
|
|
304
|
+
method,
|
|
305
|
+
]
|
|
306
|
+
|
|
307
|
+
if verbose:
|
|
308
|
+
cmd.append("--verbose")
|
|
309
|
+
|
|
310
|
+
# Run Rust solver
|
|
311
|
+
print("Running Bloomr CPP solver...")
|
|
312
|
+
print(f"Using graph: {graphml_path}")
|
|
313
|
+
print(f"Output directory: {output_dir}")
|
|
314
|
+
print() # Blank line before Rust output
|
|
315
|
+
|
|
316
|
+
try:
|
|
317
|
+
# Run without capturing output so progress bars show in real-time
|
|
318
|
+
subprocess.run(
|
|
319
|
+
cmd,
|
|
320
|
+
check=True,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
except subprocess.CalledProcessError as e:
|
|
324
|
+
raise RuntimeError(f"Rust solver failed with exit code {e.returncode}") from e
|
|
325
|
+
|
|
326
|
+
# Load results
|
|
327
|
+
gpx_path = output_dir / f"{region}_cpp_route.gpx"
|
|
328
|
+
metrics_path = output_dir / f"{region}_metrics.json"
|
|
329
|
+
|
|
330
|
+
if not gpx_path.exists():
|
|
331
|
+
raise RuntimeError(f"Expected GPX file not found: {gpx_path}")
|
|
332
|
+
|
|
333
|
+
if not metrics_path.exists():
|
|
334
|
+
raise RuntimeError(f"Expected metrics file not found: {metrics_path}")
|
|
335
|
+
|
|
336
|
+
# Load metrics
|
|
337
|
+
with open(metrics_path, "r") as f:
|
|
338
|
+
metrics = json.load(f)
|
|
339
|
+
|
|
340
|
+
# Generate visualization if requested
|
|
341
|
+
map_path = None
|
|
342
|
+
if visualize:
|
|
343
|
+
try:
|
|
344
|
+
from .visualize import plot_route_map
|
|
345
|
+
|
|
346
|
+
map_path = output_dir / f"{region}_route_map.png"
|
|
347
|
+
print("\nGenerating route visualization...")
|
|
348
|
+
plot_route_map(
|
|
349
|
+
graphml_path=graphml_path,
|
|
350
|
+
gpx_path=gpx_path,
|
|
351
|
+
output_path=map_path,
|
|
352
|
+
)
|
|
353
|
+
except ImportError:
|
|
354
|
+
print(
|
|
355
|
+
"\nWarning: Could not generate visualization. "
|
|
356
|
+
"Install visualization dependencies with:\n"
|
|
357
|
+
" pip install 'bloomr[viz]'"
|
|
358
|
+
)
|
|
359
|
+
except Exception as e:
|
|
360
|
+
print(f"\nWarning: Could not generate visualization: {e}")
|
|
361
|
+
|
|
362
|
+
# Create result object
|
|
363
|
+
cpp_result = CPPResult(
|
|
364
|
+
graphml_path=graphml_path,
|
|
365
|
+
gpx_path=gpx_path,
|
|
366
|
+
metrics_path=metrics_path,
|
|
367
|
+
metrics=metrics,
|
|
368
|
+
region=region,
|
|
369
|
+
map_path=map_path,
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
print("\nSolution completed successfully!")
|
|
373
|
+
print(cpp_result.summary())
|
|
374
|
+
|
|
375
|
+
return cpp_result
|
bloomr/visualize.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Visualization utilities for CPP solutions.
|
|
3
|
+
|
|
4
|
+
This module provides functions to visualize CPP routes on maps.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import xml.etree.ElementTree as ET
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import List, Tuple
|
|
10
|
+
|
|
11
|
+
import networkx as nx
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def plot_route_map(
|
|
15
|
+
graphml_path: Path,
|
|
16
|
+
gpx_path: Path,
|
|
17
|
+
output_path: Path,
|
|
18
|
+
*,
|
|
19
|
+
figsize: Tuple[int, int] = (15, 15),
|
|
20
|
+
show_basemap: bool = True,
|
|
21
|
+
route_color: str = "#FF0000",
|
|
22
|
+
route_alpha: float = 0.6,
|
|
23
|
+
route_width: float = 1.5,
|
|
24
|
+
network_color: str = "#CCCCCC",
|
|
25
|
+
network_alpha: float = 0.3,
|
|
26
|
+
network_width: float = 0.5,
|
|
27
|
+
dpi: int = 150,
|
|
28
|
+
) -> Path:
|
|
29
|
+
"""
|
|
30
|
+
Create a map visualization of the CPP route.
|
|
31
|
+
|
|
32
|
+
This function reads the GraphML road network and GPX route file,
|
|
33
|
+
then creates a visualization showing the route overlaid on the road network
|
|
34
|
+
with an optional basemap.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
graphml_path: Path to the GraphML file containing the road network
|
|
38
|
+
gpx_path: Path to the GPX file containing the route
|
|
39
|
+
output_path: Path where the PNG image will be saved
|
|
40
|
+
figsize: Figure size in inches (width, height)
|
|
41
|
+
show_basemap: Whether to show a basemap (requires contextily)
|
|
42
|
+
route_color: Color for the route line
|
|
43
|
+
route_alpha: Transparency of the route line (0-1)
|
|
44
|
+
route_width: Width of the route line
|
|
45
|
+
network_color: Color for the underlying road network
|
|
46
|
+
network_alpha: Transparency of the road network (0-1)
|
|
47
|
+
network_width: Width of the road network lines
|
|
48
|
+
dpi: Dots per inch for the output image
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Path to the saved PNG file
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
ImportError: If required visualization dependencies are not installed
|
|
55
|
+
FileNotFoundError: If input files don't exist
|
|
56
|
+
|
|
57
|
+
Examples:
|
|
58
|
+
>>> from bloomr.visualize import plot_route_map
|
|
59
|
+
>>> from pathlib import Path
|
|
60
|
+
>>>
|
|
61
|
+
>>> plot_route_map(
|
|
62
|
+
... graphml_path=Path("graphml_data/jersey.graphml"),
|
|
63
|
+
... gpx_path=Path("solutions/jersey/jersey_cpp_route.gpx"),
|
|
64
|
+
... output_path=Path("solutions/jersey/jersey_route_map.png"),
|
|
65
|
+
... )
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
import matplotlib.patches as mpatches
|
|
69
|
+
import matplotlib.pyplot as plt
|
|
70
|
+
except ImportError as e:
|
|
71
|
+
raise ImportError(
|
|
72
|
+
"Visualization requires matplotlib. Install with:\n" " pip install 'bloomr[viz]'"
|
|
73
|
+
) from e
|
|
74
|
+
|
|
75
|
+
if show_basemap:
|
|
76
|
+
try:
|
|
77
|
+
import contextily as ctx
|
|
78
|
+
except ImportError as e:
|
|
79
|
+
raise ImportError(
|
|
80
|
+
"Basemap visualization requires contextily. Install with:\n"
|
|
81
|
+
" pip install 'bloomr[viz]'"
|
|
82
|
+
) from e
|
|
83
|
+
|
|
84
|
+
# Verify input files exist
|
|
85
|
+
if not graphml_path.exists():
|
|
86
|
+
raise FileNotFoundError(f"GraphML file not found: {graphml_path}")
|
|
87
|
+
if not gpx_path.exists():
|
|
88
|
+
raise FileNotFoundError(f"GPX file not found: {gpx_path}")
|
|
89
|
+
|
|
90
|
+
# Load the road network
|
|
91
|
+
print(f"Loading road network from {graphml_path.name}...")
|
|
92
|
+
G = nx.read_graphml(graphml_path)
|
|
93
|
+
|
|
94
|
+
# Extract coordinates from the graph
|
|
95
|
+
node_positions = {}
|
|
96
|
+
for node, data in G.nodes(data=True):
|
|
97
|
+
try:
|
|
98
|
+
lon = float(data.get("x", data.get("d4", 0)))
|
|
99
|
+
lat = float(data.get("y", data.get("d3", 0)))
|
|
100
|
+
node_positions[node] = (lon, lat)
|
|
101
|
+
except (ValueError, TypeError):
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
# Parse GPX file to get route coordinates
|
|
105
|
+
print(f"Loading route from {gpx_path.name}...")
|
|
106
|
+
route_coords = _parse_gpx(gpx_path)
|
|
107
|
+
|
|
108
|
+
if not route_coords:
|
|
109
|
+
raise ValueError(f"No route coordinates found in GPX file: {gpx_path}")
|
|
110
|
+
|
|
111
|
+
print(f"Creating visualization with {len(route_coords)} route points...")
|
|
112
|
+
|
|
113
|
+
# Create figure
|
|
114
|
+
fig, ax = plt.subplots(figsize=figsize, dpi=dpi)
|
|
115
|
+
|
|
116
|
+
# Plot road network edges
|
|
117
|
+
for u, v in G.edges():
|
|
118
|
+
if u in node_positions and v in node_positions:
|
|
119
|
+
x_coords = [node_positions[u][0], node_positions[v][0]]
|
|
120
|
+
y_coords = [node_positions[u][1], node_positions[v][1]]
|
|
121
|
+
ax.plot(
|
|
122
|
+
x_coords,
|
|
123
|
+
y_coords,
|
|
124
|
+
color=network_color,
|
|
125
|
+
alpha=network_alpha,
|
|
126
|
+
linewidth=network_width,
|
|
127
|
+
zorder=1,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Plot route
|
|
131
|
+
route_lons = [coord[0] for coord in route_coords]
|
|
132
|
+
route_lats = [coord[1] for coord in route_coords]
|
|
133
|
+
ax.plot(
|
|
134
|
+
route_lons,
|
|
135
|
+
route_lats,
|
|
136
|
+
color=route_color,
|
|
137
|
+
alpha=route_alpha,
|
|
138
|
+
linewidth=route_width,
|
|
139
|
+
zorder=2,
|
|
140
|
+
label="CPP Route",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Mark start/end points
|
|
144
|
+
if route_coords:
|
|
145
|
+
start = route_coords[0]
|
|
146
|
+
end = route_coords[-1]
|
|
147
|
+
ax.plot(start[0], start[1], "go", markersize=10, zorder=3, label="Start")
|
|
148
|
+
ax.plot(end[0], end[1], "rs", markersize=10, zorder=3, label="End")
|
|
149
|
+
|
|
150
|
+
# Add basemap if requested
|
|
151
|
+
if show_basemap:
|
|
152
|
+
try:
|
|
153
|
+
print("Adding basemap (this may take a moment)...")
|
|
154
|
+
ctx.add_basemap(
|
|
155
|
+
ax,
|
|
156
|
+
crs="EPSG:4326",
|
|
157
|
+
source=ctx.providers.CartoDB.Positron,
|
|
158
|
+
attribution=False,
|
|
159
|
+
)
|
|
160
|
+
except Exception as e:
|
|
161
|
+
print(f"Warning: Could not add basemap: {e}")
|
|
162
|
+
print("Continuing without basemap...")
|
|
163
|
+
|
|
164
|
+
# Set labels and title
|
|
165
|
+
ax.set_xlabel("Longitude", fontsize=12)
|
|
166
|
+
ax.set_ylabel("Latitude", fontsize=12)
|
|
167
|
+
ax.set_title("Chinese Postman Problem Solution", fontsize=16, fontweight="bold")
|
|
168
|
+
|
|
169
|
+
# Add legend
|
|
170
|
+
legend_elements = [
|
|
171
|
+
mpatches.Patch(color=route_color, alpha=route_alpha, label="CPP Route"),
|
|
172
|
+
mpatches.Patch(color=network_color, alpha=network_alpha, label="Road Network"),
|
|
173
|
+
plt.Line2D(
|
|
174
|
+
[0], [0], marker="o", color="w", markerfacecolor="g", markersize=8, label="Start"
|
|
175
|
+
),
|
|
176
|
+
plt.Line2D([0], [0], marker="s", color="w", markerfacecolor="r", markersize=8, label="End"),
|
|
177
|
+
]
|
|
178
|
+
ax.legend(handles=legend_elements, loc="upper right", fontsize=10)
|
|
179
|
+
|
|
180
|
+
# Adjust layout and save
|
|
181
|
+
plt.tight_layout()
|
|
182
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
183
|
+
print(f"Saving map to {output_path}...")
|
|
184
|
+
plt.savefig(output_path, dpi=dpi, bbox_inches="tight")
|
|
185
|
+
plt.close()
|
|
186
|
+
|
|
187
|
+
print(f"Map saved successfully: {output_path}")
|
|
188
|
+
return output_path
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _parse_gpx(gpx_path: Path) -> List[Tuple[float, float]]:
|
|
192
|
+
"""
|
|
193
|
+
Parse GPX file and extract route coordinates.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
gpx_path: Path to GPX file
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
List of (lon, lat) tuples
|
|
200
|
+
"""
|
|
201
|
+
# GPX files are generated by our own Rust backend, not untrusted external sources
|
|
202
|
+
tree = ET.parse(gpx_path) # nosec B314
|
|
203
|
+
root = tree.getroot()
|
|
204
|
+
|
|
205
|
+
# GPX namespace
|
|
206
|
+
ns = {"gpx": "http://www.topografix.com/GPX/1/1"}
|
|
207
|
+
|
|
208
|
+
coords = []
|
|
209
|
+
|
|
210
|
+
# Try to find track points
|
|
211
|
+
for trkpt in root.findall(".//gpx:trkpt", ns):
|
|
212
|
+
lat_str = trkpt.get("lat")
|
|
213
|
+
lon_str = trkpt.get("lon")
|
|
214
|
+
if lat_str is not None and lon_str is not None:
|
|
215
|
+
lat = float(lat_str)
|
|
216
|
+
lon = float(lon_str)
|
|
217
|
+
coords.append((lon, lat))
|
|
218
|
+
|
|
219
|
+
# If no track points, try route points
|
|
220
|
+
if not coords:
|
|
221
|
+
for rtept in root.findall(".//gpx:rtept", ns):
|
|
222
|
+
lat_str = rtept.get("lat")
|
|
223
|
+
lon_str = rtept.get("lon")
|
|
224
|
+
if lat_str is not None and lon_str is not None:
|
|
225
|
+
lat = float(lat_str)
|
|
226
|
+
lon = float(lon_str)
|
|
227
|
+
coords.append((lon, lat))
|
|
228
|
+
|
|
229
|
+
# If still no points, try waypoints
|
|
230
|
+
if not coords:
|
|
231
|
+
for wpt in root.findall(".//gpx:wpt", ns):
|
|
232
|
+
lat_str = wpt.get("lat")
|
|
233
|
+
lon_str = wpt.get("lon")
|
|
234
|
+
if lat_str is not None and lon_str is not None:
|
|
235
|
+
lat = float(lat_str)
|
|
236
|
+
lon = float(lon_str)
|
|
237
|
+
coords.append((lon, lat))
|
|
238
|
+
|
|
239
|
+
return coords
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bloomr
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Memory-efficient Chinese Postman Problem solver using Rust blossom algorithm
|
|
5
|
+
Author-email: Nathaniel Hey <gnathoi@nat.je>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/gnathoi/bloomr
|
|
8
|
+
Project-URL: Documentation, https://github.com/gnathoi/bloomr#readme
|
|
9
|
+
Project-URL: Repository, https://github.com/gnathoi/bloomr
|
|
10
|
+
Project-URL: Issues, https://github.com/gnathoi/bloomr/issues
|
|
11
|
+
Keywords: chinese-postman,graph-algorithms,routing,osmnx,blossom-algorithm,eulerian-circuit
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Rust
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: osmnx>=1.9.0
|
|
26
|
+
Requires-Dist: networkx>=3.0
|
|
27
|
+
Provides-Extra: viz
|
|
28
|
+
Requires-Dist: matplotlib>=3.5.0; extra == "viz"
|
|
29
|
+
Requires-Dist: contextily>=1.3.0; extra == "viz"
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
32
|
+
Requires-Dist: black>=23.0; extra == "dev"
|
|
33
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
34
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# Bloomr
|
|
38
|
+
|
|
39
|
+
[](https://github.com/gnathoi/bloomr/actions/workflows/ci.yml)
|
|
40
|
+
[](https://www.python.org/downloads/)
|
|
41
|
+
[](https://pypi.org/project/bloomr/)
|
|
42
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
|
43
|
+
[](https://www.rust-lang.org/)
|
|
44
|
+
|
|
45
|
+
A Python package for solving the Mixed Chinese Postman Problem on road networks with a high-performance Rust backend.
|
|
46
|
+
|
|
47
|
+
## What is the Mixed Chinese Postman Problem?
|
|
48
|
+
|
|
49
|
+
The Chinese Postman Problem asks: what is the shortest route that traverses every edge in a graph at least once? This is useful for planning routes that need to, for example, survey every road in an area as efficiently as possible.
|
|
50
|
+
|
|
51
|
+
The Mixed CPP extends this to real-world road networks where:
|
|
52
|
+
|
|
53
|
+
- Some streets are bidirectional (two-way streets that can be traversed in either direction)
|
|
54
|
+
- Some streets are directed (one-way streets with mandatory direction)
|
|
55
|
+
|
|
56
|
+
Finding optimal routes in mixed networks is more complex than the classical all-directed or all-undirected versions of the problem. Bloomr solves this using Edmonds' blossom algorithm for efficient minimum-weight perfect matching.
|
|
57
|
+
|
|
58
|
+
## What Bloomr Does
|
|
59
|
+
|
|
60
|
+
Bloomr downloads real road network data from OpenStreetMap, identifies one-way and two-way streets, computes an optimal route that covers every street at least once, and outputs the solution as a GPX file for GPS navigation.
|
|
61
|
+
|
|
62
|
+
The package uses a Python API for ease of use and a Rust backend for computational performance. The Rust implementation uses the blossom algorithm to find perfect matchings that minimize the total distance of repeated streets.
|
|
63
|
+
|
|
64
|
+
## Installation
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install bloomr
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Python API
|
|
71
|
+
|
|
72
|
+
The API provides two main functions:
|
|
73
|
+
|
|
74
|
+
### Solve CPP for a location
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from bloomr import solve_cpp
|
|
78
|
+
|
|
79
|
+
# Download road network and solve CPP
|
|
80
|
+
result = solve_cpp("Jersey, Channel Islands")
|
|
81
|
+
|
|
82
|
+
# View solution summary
|
|
83
|
+
print(result.summary())
|
|
84
|
+
|
|
85
|
+
# Access result files
|
|
86
|
+
print(f"Route saved to: {result.gpx_path}")
|
|
87
|
+
print(f"Metrics: {result.metrics}")
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Download a graph separately
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from bloomr import download_graph
|
|
94
|
+
|
|
95
|
+
# Download and cache a road network
|
|
96
|
+
graph_path = download_graph("San Francisco, California")
|
|
97
|
+
|
|
98
|
+
# Solve using the cached graph
|
|
99
|
+
result = solve_cpp(graphml_path=graph_path)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## API Reference
|
|
103
|
+
|
|
104
|
+
### solve_cpp()
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
solve_cpp(
|
|
108
|
+
place: Optional[str] = None,
|
|
109
|
+
*,
|
|
110
|
+
graphml_path: Optional[Path] = None,
|
|
111
|
+
output_dir: Optional[Path] = None,
|
|
112
|
+
network_type: str = "drive_service",
|
|
113
|
+
simplify: bool = False,
|
|
114
|
+
method: str = "blossom",
|
|
115
|
+
verbose: bool = False,
|
|
116
|
+
visualize: bool = True
|
|
117
|
+
)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Solve the Chinese Postman Problem for a road network.
|
|
121
|
+
|
|
122
|
+
Arguments:
|
|
123
|
+
|
|
124
|
+
- `place`: Location name for OSMnx (e.g., "Jersey, Channel Islands")
|
|
125
|
+
- `graphml_path`: Path to existing GraphML file (alternative to place)
|
|
126
|
+
- `output_dir`: Directory for output files (default: "solutions/{region}")
|
|
127
|
+
- `network_type`: Type of road network - "drive", "drive_service", "walk", "bike", or "all"
|
|
128
|
+
- `simplify`: Whether to simplify the graph topology
|
|
129
|
+
- `method`: Graph balancing algorithm - "blossom" (optimal, default)
|
|
130
|
+
- `verbose`: Print detailed progress information
|
|
131
|
+
- `visualize`: Generate a route visualization map
|
|
132
|
+
|
|
133
|
+
Returns a `CPPResult` object containing:
|
|
134
|
+
|
|
135
|
+
- `graphml_path`: Input graph file
|
|
136
|
+
- `gpx_path`: Output GPX route file
|
|
137
|
+
- `metrics_path`: JSON file with solution metrics
|
|
138
|
+
- `metrics`: Dictionary of solution metrics
|
|
139
|
+
- `region`: Region name
|
|
140
|
+
- `map_path`: Path to visualization (if generated)
|
|
141
|
+
|
|
142
|
+
### download_graph()
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
download_graph(
|
|
146
|
+
place: str,
|
|
147
|
+
*,
|
|
148
|
+
output_dir: Optional[Path] = None,
|
|
149
|
+
network_type: str = "drive_service",
|
|
150
|
+
simplify: bool = False,
|
|
151
|
+
force: bool = False
|
|
152
|
+
) -> Path
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Download a road network from OpenStreetMap and save as GraphML.
|
|
156
|
+
|
|
157
|
+
Arguments:
|
|
158
|
+
|
|
159
|
+
- `place`: Location name for OSMnx
|
|
160
|
+
- `output_dir`: Directory for GraphML file (default: "graphml_data")
|
|
161
|
+
- `network_type`: Type of road network
|
|
162
|
+
- `simplify`: Whether to simplify the graph topology
|
|
163
|
+
- `force`: Force re-download even if cached
|
|
164
|
+
|
|
165
|
+
Returns the path to the saved GraphML file.
|
|
166
|
+
|
|
167
|
+
## Solution Metrics
|
|
168
|
+
|
|
169
|
+
Each solution includes comprehensive metrics:
|
|
170
|
+
|
|
171
|
+
- `unique_road_segments`: Total number of distinct streets
|
|
172
|
+
- `bidirectional_edge_pairs`: Number of two-way streets
|
|
173
|
+
- `one_way_edges`: Number of one-way streets
|
|
174
|
+
- `duplication_ratio`: Average times each street is traversed
|
|
175
|
+
- `total_original_distance_km`: Total length of all streets (traversed once)
|
|
176
|
+
- `total_circuit_distance_km`: Actual route distance
|
|
177
|
+
- `distance_efficiency`: Ratio of original to circuit distance
|
|
178
|
+
- `deadhead_percentage`: Percentage of route that repeats streets
|
|
179
|
+
|
|
180
|
+
## Output Files
|
|
181
|
+
|
|
182
|
+
All outputs are stored in `solutions/{region_name}/`:
|
|
183
|
+
|
|
184
|
+
- `{region}_cpp_route.gpx`: GPS route file for navigation
|
|
185
|
+
- `{region}_metrics.json`: Detailed solution metrics
|
|
186
|
+
|
|
187
|
+
Graphs are cached in `graphml_data/` to avoid re-downloading.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
bloomr/__init__.py,sha256=Y1VohmfGPdw0v7mHAG-0-6FliOWY5h-tBnqs-T8AZ38,1463
|
|
2
|
+
bloomr/download.py,sha256=fZA1SJ0BTg9iwtA8lErLSm2Rep-m4aiCa2gdCSoMHLw,3920
|
|
3
|
+
bloomr/solver.py,sha256=FRi7UnZGUjwe7If_2OOLCZQaGGUBymHDKt57jw6osMY,12180
|
|
4
|
+
bloomr/visualize.py,sha256=02G9wg2xwloKQUNlTplFza8O0ASZgTBv927EAgioEic,7874
|
|
5
|
+
bloomr/bin/bloomr-rust-linux-x86_64,sha256=Mhr_lRkgcLZpI9xOqMYBYUKuZR7siCZC6_nYgu5-FLM,1636288
|
|
6
|
+
bloomr/bin/bloomr-rust-macos-aarch64,sha256=MK5ugHRdYN98MwT50lFzhEd9HuLQZgR07Ski6oakiyA,1388128
|
|
7
|
+
bloomr/bin/bloomr-rust-macos-x86_64,sha256=lHgs1Z-nzY9EU6NsIZl0zq8NU4TkzyDBCgn5DgPHsUY,1481368
|
|
8
|
+
bloomr/bin/bloomr-rust-windows-x86_64.exe,sha256=fMwEBUpaXe5uG7jE_v2Y3vPeRaqb77H_xIsjxyQLLrc,1349120
|
|
9
|
+
bloomr-0.1.1.dist-info/licenses/LICENSE,sha256=6kwgzDLrIVTZSRWSydWtFbAetQRjIqDhls-AqwA3Wes,11341
|
|
10
|
+
bloomr-0.1.1.dist-info/METADATA,sha256=9__lg6SNSzpVlVnZ8eS5lup819ckoMJJkP9Y3h1D_mk,6682
|
|
11
|
+
bloomr-0.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
12
|
+
bloomr-0.1.1.dist-info/top_level.txt,sha256=sL9zHoV1EwZQxZZouGQ2hUQMNl-a0QABft1-XKWdl94,7
|
|
13
|
+
bloomr-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Support. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright 2025 Nathaniel Hey
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
bloomr
|