mirror of
https://github.com/zvx-echo6/recon.git
synced 2026-05-20 06:34:40 +02:00
feat(offroute): Phase O3a — trail burn-in, pathfinder seeks trail corridors
Trail friction REPLACES land cover friction where trails exist: - Road (value 5): 0.1× friction - Track (value 15): 0.3× friction - Foot trail (value 25): 0.5× friction TrailReader loads /mnt/nav/worldcover/trails.tif rasterized from OSM highways. Validation shows trail-seeking behavior: - On-trail travel: 17.3% → 98.7% - Effort time: 1047 min → 155 min (-85.2%) - Path travels farther but stays on roads for speed Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
e0eedcedfd
commit
3293cb4238
3 changed files with 392 additions and 160 deletions
|
|
@ -3,7 +3,7 @@ Tobler off-path hiking cost function for OFFROUTE.
|
|||
|
||||
Computes travel time cost based on terrain slope using Tobler's
|
||||
hiking function with off-trail penalty. Optionally applies friction
|
||||
multipliers from land cover data and barrier grids from PAD-US.
|
||||
multipliers from land cover data, trail corridors, and barrier grids.
|
||||
"""
|
||||
import math
|
||||
import numpy as np
|
||||
|
|
@ -19,6 +19,14 @@ TOBLER_OFF_TRAIL_MULT = 0.6
|
|||
# Pragmatic mode friction multiplier for private land
|
||||
PRAGMATIC_BARRIER_MULTIPLIER = 5.0
|
||||
|
||||
# Trail value to friction multiplier mapping
|
||||
# Trail friction REPLACES land cover friction (a road through forest is still easy)
|
||||
TRAIL_FRICTION_MAP = {
|
||||
5: 0.1, # road
|
||||
15: 0.3, # track
|
||||
25: 0.5, # foot trail
|
||||
}
|
||||
|
||||
|
||||
def tobler_speed(grade: float) -> float:
|
||||
"""
|
||||
|
|
@ -37,6 +45,7 @@ def compute_cost_grid(
|
|||
cell_size_lat_m: float = None,
|
||||
cell_size_lon_m: float = None,
|
||||
friction: Optional[np.ndarray] = None,
|
||||
trails: Optional[np.ndarray] = None,
|
||||
barriers: Optional[np.ndarray] = None,
|
||||
boundary_mode: Literal["strict", "pragmatic", "emergency"] = "pragmatic"
|
||||
) -> np.ndarray:
|
||||
|
|
@ -51,10 +60,17 @@ def compute_cost_grid(
|
|||
cell_size_m: Average cell size in meters
|
||||
cell_size_lat_m: Cell size in latitude direction (optional)
|
||||
cell_size_lon_m: Cell size in longitude direction (optional)
|
||||
friction: Optional 2D array of friction multipliers.
|
||||
friction: Optional 2D array of friction multipliers (WorldCover).
|
||||
Values should be float (1.0 = baseline, 2.0 = 2x slower).
|
||||
np.inf marks impassable cells.
|
||||
If None, no friction is applied (backward compatible).
|
||||
trails: Optional 2D array of trail values (uint8).
|
||||
0 = no trail (use friction)
|
||||
5 = road (0.1× friction, replaces WorldCover)
|
||||
15 = track (0.3× friction, replaces WorldCover)
|
||||
25 = foot trail (0.5× friction, replaces WorldCover)
|
||||
Trail friction REPLACES land cover friction where trails exist.
|
||||
If None, no trail burn-in is applied.
|
||||
barriers: Optional 2D array of barrier values (uint8).
|
||||
255 = closed/restricted area (from PAD-US Pub_Access = XA).
|
||||
0 = accessible.
|
||||
|
|
@ -111,14 +127,30 @@ def compute_cost_grid(
|
|||
# Handle NaN elevations (no data)
|
||||
cost[np.isnan(elevation)] = np.inf
|
||||
|
||||
# Apply friction multipliers if provided
|
||||
# Build effective friction array
|
||||
# Start with WorldCover friction if provided, else 1.0
|
||||
if friction is not None:
|
||||
if friction.shape != elevation.shape:
|
||||
raise ValueError(
|
||||
f"Friction shape {friction.shape} does not match elevation shape {elevation.shape}"
|
||||
)
|
||||
# Multiply cost by friction (inf * anything = inf, which is correct)
|
||||
cost = cost * friction
|
||||
effective_friction = friction.copy()
|
||||
else:
|
||||
effective_friction = np.ones(elevation.shape, dtype=np.float32)
|
||||
|
||||
# Apply trail burn-in: trails REPLACE land cover friction
|
||||
if trails is not None:
|
||||
if trails.shape != elevation.shape:
|
||||
raise ValueError(
|
||||
f"Trails shape {trails.shape} does not match elevation shape {elevation.shape}"
|
||||
)
|
||||
# Replace friction where trails exist
|
||||
for trail_value, trail_friction in TRAIL_FRICTION_MAP.items():
|
||||
trail_mask = trails == trail_value
|
||||
effective_friction[trail_mask] = trail_friction
|
||||
|
||||
# Apply friction to cost
|
||||
cost = cost * effective_friction
|
||||
|
||||
# Apply barriers based on boundary_mode
|
||||
if barriers is not None and boundary_mode != "emergency":
|
||||
|
|
@ -145,7 +177,7 @@ if __name__ == "__main__":
|
|||
speed = tobler_speed(grade)
|
||||
print(f" Grade {grade:+.2f}: {speed:.2f} km/h")
|
||||
|
||||
print("\nTesting cost grid computation (no friction, no barriers):")
|
||||
print("\nTesting cost grid computation (no friction, no trails):")
|
||||
elev = np.arange(100).reshape(10, 10).astype(np.float32) * 10
|
||||
cost = compute_cost_grid(elev, cell_size_m=30.0)
|
||||
print(f" Elevation range: {elev.min():.0f} - {elev.max():.0f} m")
|
||||
|
|
@ -155,21 +187,25 @@ if __name__ == "__main__":
|
|||
else:
|
||||
print(f" All cells impassable (test data too steep)")
|
||||
|
||||
print("\nTesting cost grid with friction:")
|
||||
print("\nTesting cost grid with friction and trails:")
|
||||
elev = np.ones((10, 10), dtype=np.float32) * 1000 # flat terrain
|
||||
friction = np.ones((10, 10), dtype=np.float32) * 1.5 # 1.5x friction
|
||||
friction[5, 5] = np.inf # one impassable cell
|
||||
cost = compute_cost_grid(elev, cell_size_m=30.0, friction=friction)
|
||||
print(f" Base cost (flat, 30m cell): {30 * 3.6 / (0.6 * 6.0 * np.exp(-3.5 * 0.05)):.1f} s")
|
||||
print(f" With 1.5x friction: {cost[0, 0]:.1f} s")
|
||||
print(f" Impassable cells: {np.sum(np.isinf(cost))}")
|
||||
friction = np.ones((10, 10), dtype=np.float32) * 2.0 # 2.0x friction (forest)
|
||||
trails = np.zeros((10, 10), dtype=np.uint8)
|
||||
trails[5, :] = 5 # road across middle row
|
||||
|
||||
print("\nTesting cost grid with barriers (three modes):")
|
||||
elev = np.ones((10, 10), dtype=np.float32) * 1000 # flat terrain
|
||||
barriers = np.zeros((10, 10), dtype=np.uint8)
|
||||
barriers[3:7, 3:7] = 255 # 4x4 closed area in center
|
||||
cost_no_trail = compute_cost_grid(elev, cell_size_m=30.0, friction=friction)
|
||||
cost_with_trail = compute_cost_grid(elev, cell_size_m=30.0, friction=friction, trails=trails)
|
||||
|
||||
base_cost = 30 * 3.6 / (0.6 * 6.0 * np.exp(-3.5 * 0.05))
|
||||
print(f" Base cost (flat, 30m cell): {base_cost:.1f} s")
|
||||
print(f" Forest cell (2.0x friction): {cost_no_trail[0, 0]:.1f} s")
|
||||
print(f" Road cell (0.1x friction, replaces forest): {cost_with_trail[5, 0]:.1f} s")
|
||||
print(f" Road friction advantage: {cost_no_trail[0, 0] / cost_with_trail[5, 0]:.1f}x faster")
|
||||
|
||||
print("\nTesting cost grid with barriers (three modes):")
|
||||
elev = np.ones((10, 10), dtype=np.float32) * 1000
|
||||
barriers = np.zeros((10, 10), dtype=np.uint8)
|
||||
barriers[3:7, 3:7] = 255
|
||||
|
||||
for mode in ["strict", "pragmatic", "emergency"]:
|
||||
cost = compute_cost_grid(elev, cell_size_m=30.0, barriers=barriers, boundary_mode=mode)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue