recon/lib/offroute/cost.py

94 lines
3.2 KiB
Python
Raw Normal View History

"""
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.
"""
import math
import numpy as np
from typing import Tuple
# Maximum passable slope in degrees
MAX_SLOPE_DEG = 40.0
# Tobler off-path parameters
TOBLER_BASE_SPEED = 6.0
TOBLER_OFF_TRAIL_MULT = 0.6
def tobler_speed(grade: float) -> float:
"""
Calculate hiking speed using Tobler's off-path function.
speed_kmh = 0.6 * 6.0 * exp(-3.5 * |grade + 0.05|)
Peak speed is ~3.6 km/h at grade = -0.05 (slight downhill).
"""
return TOBLER_OFF_TRAIL_MULT * TOBLER_BASE_SPEED * math.exp(-3.5 * abs(grade + 0.05))
def compute_cost_grid(
elevation: np.ndarray,
cell_size_m: float,
cell_size_lat_m: float = None,
cell_size_lon_m: float = None
) -> np.ndarray:
"""
Compute isotropic travel cost grid from elevation data.
Each cell's cost represents the time (in seconds) to traverse that cell,
based on the average slope from neighboring cells.
"""
if cell_size_lat_m is None:
cell_size_lat_m = cell_size_m
if cell_size_lon_m is None:
cell_size_lon_m = cell_size_m
rows, cols = elevation.shape
# Compute gradients in both directions
dy = np.zeros_like(elevation)
dx = np.zeros_like(elevation)
# Central differences for interior, forward/backward at edges
dy[1:-1, :] = (elevation[:-2, :] - elevation[2:, :]) / (2 * cell_size_lat_m)
dy[0, :] = (elevation[0, :] - elevation[1, :]) / cell_size_lat_m
dy[-1, :] = (elevation[-2, :] - elevation[-1, :]) / cell_size_lat_m
dx[:, 1:-1] = (elevation[:, 2:] - elevation[:, :-2]) / (2 * cell_size_lon_m)
dx[:, 0] = (elevation[:, 1] - elevation[:, 0]) / cell_size_lon_m
dx[:, -1] = (elevation[:, -1] - elevation[:, -2]) / cell_size_lon_m
# Compute slope magnitude (grade = rise/run)
grade_magnitude = np.sqrt(dx**2 + dy**2)
# Convert to slope angle in degrees
slope_deg = np.degrees(np.arctan(grade_magnitude))
# Compute speed for each cell using Tobler function
speed_kmh = TOBLER_OFF_TRAIL_MULT * TOBLER_BASE_SPEED * np.exp(-3.5 * np.abs(grade_magnitude + 0.05))
# Convert speed to time cost (seconds to traverse one cell)
avg_cell_size = (cell_size_lat_m + cell_size_lon_m) / 2
cost = avg_cell_size * 3.6 / speed_kmh
# Set impassable cells (slope > MAX_SLOPE_DEG) to infinity
cost[slope_deg > MAX_SLOPE_DEG] = np.inf
# Handle NaN elevations (no data)
cost[np.isnan(elevation)] = np.inf
return cost
if __name__ == "__main__":
print("Testing Tobler speed function:")
for grade in [-0.3, -0.1, -0.05, 0.0, 0.05, 0.1, 0.3]:
speed = tobler_speed(grade)
print(f" Grade {grade:+.2f}: {speed:.2f} km/h")
print("\nTesting cost grid computation:")
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")
print(f" Cost range: {cost[~np.isinf(cost)].min():.1f} - {cost[~np.isinf(cost)].max():.1f} s")