Skip to main content

Overview

The terrain engine provides real topographic horizon profile computation by loading ICGC 5×5 DEM tiles (ESRI ASCII Grid .txt), caching them as .npy files, and computing multi-band horizon silhouettes via raycasting with Earth curvature correction.

Constants

R_EARTH
float
default:"6371000.0"
Earth radius in meters used for curvature correction

Functions

generate_bands

generate_bands(n: int = 20, max_dist_m: float = 150_000) -> list
Generates N horizon bands with logarithmic distribution across zones (piecewise bilog).

Zones

  • Near zone (0 → 5km): 2/3 of the N bands. Captures foreground detail (hills, cliffs, passes).
  • Far zone (5km → max): 1/3 of the N bands. Represents atmospheric haze with fewer bands.
n
int
default:"20"
Total number of bands
  • 10 = Low quality
  • 20 = Normal quality
  • 40 = High quality
  • 60 = Ultra quality
  • 80 = Extreme quality
max_dist_m
float
default:"150000"
Maximum calculation distance in meters

Returns

List of dictionaries with:
  • id (str): Band identifier (e.g., “gnd_0_71”, “near_144_208”, “far_25k_38k”)
  • min (float): Minimum distance in meters
  • max (float): Maximum distance in meters

Example

from TerraLab.terrain.engine import generate_bands

# Generate 40 bands for high quality
bands = generate_bands(n=40, max_dist_m=150_000)

for band in bands[:3]:
    print(f"{band['id']}: {band['min']:.0f}m - {band['max']:.0f}m")
# Output:
# gnd_0_1: 0m - 1m
# gnd_1_2: 1m - 2m
# gnd_2_4: 2m - 4m

bake_and_save

bake_and_save(
    lat: float,
    lon: float,
    tiles_dir: str,
    output_path: str,
    radius: float = 100_000,
    step_m: float = 50,
    resolution_deg: float = 0.5,
    eye_height: float = 1.7,
    band_defs: Optional[List[Dict]] = None
)
Full pipeline: transforms coordinates, indexes tiles, bakes horizon profile, and saves to .npz file.
lat
float
required
Observer latitude (WGS84)
lon
float
required
Observer longitude (WGS84)
tiles_dir
str
required
Directory containing DEM tiles
output_path
str
required
Path to save the .npz profile file
radius
float
default:"100000"
Maximum raycasting distance in meters
step_m
float
default:"50"
Raycasting step size in meters
resolution_deg
float
default:"0.5"
Azimuth resolution in degrees
eye_height
float
default:"1.7"
Observer eye height above ground in meters
band_defs
List[Dict]
default:"None"
Optional custom band definitions. If None, uses DEFAULT_BANDS (20 bands)

Returns

HorizonProfile object containing the baked profile data.

load_profile

load_profile(path: str) -> Optional[HorizonProfile]
Loads a pre-baked horizon profile from disk.
path
str
required
Path to the .npz profile file

Returns

HorizonProfile object or None if the file is missing or invalid.

Classes

HorizonProfile

Serializable horizon profile result.

Attributes

azimuths
np.ndarray
Array of azimuths in degrees (0-360), shape (N,)
bands
List[Dict]
List of band dictionaries, each containing:
  • id (str): Band identifier
  • angles (np.ndarray): Elevation angles in radians
  • dists (np.ndarray): Distances in meters
  • heights (np.ndarray): Terrain heights in meters
observer_lat
float
default:"0.0"
Observer latitude in degrees
observer_lon
float
default:"0.0"
Observer longitude in degrees
light_domes
Optional[np.ndarray]
default:"None"
Light pollution dome values per azimuth
light_peak_distances
Optional[np.ndarray]
default:"None"
Distances of maximum light source per azimuth

Methods

get_band_points
get_band_points(band_id: str) -> List[Tuple[float, float]]
Returns list of (azimuth_deg, elevation_deg) tuples for a given band.
band_id
str
required
Band identifier (e.g., “near_144_208”)
save
save(path: str)
Saves the profile to a compressed .npz file.
path
str
required
Output file path
load (static)
@staticmethod
load(path: str) -> HorizonProfile
Loads a profile from a .npz file.
path
str
required
Path to the .npz file

HorizonBaker

Raycasts from observer position to compute horizon elevation angles. When a ray exits available DEM coverage, it stops and keeps whatever silhouette data was already gathered.

Constructor

HorizonBaker(
    provider,
    eye_height: float = 1.7,
    R: float = R_EARTH
)
provider
DemSampler
required
DEM elevation provider with get_elevation(x, y) method
eye_height
float
default:"1.7"
Observer eye height above ground in meters
R
float
default:"6371000.0"
Earth radius in meters for curvature correction

Methods

bake
bake(
    obs_x: float,
    obs_y: float,
    obs_h_ground: Optional[float] = None,
    step_m: float = 50,
    d_max: float = 100_000,
    delta_az_deg: float = 0.5,
    band_defs: Optional[List[Dict]] = None,
    progress_callback = None,
    light_sampler = None,
    abort_check = None
) -> Tuple[np.ndarray, List[Dict], np.ndarray]
Computes multi-band horizon profile using raycasting.
obs_x
float
required
Observer X coordinate in projected CRS (e.g., UTM)
obs_y
float
required
Observer Y coordinate in projected CRS
obs_h_ground
Optional[float]
default:"None"
Observer ground elevation. If None, sampled from DEM
step_m
float
default:"50"
Initial raycasting step size in meters. Adaptive stepping is used: starts at 0.5m, grows to step_m, then multiplies for distant ranges
d_max
float
default:"100000"
Maximum raycasting distance in meters
delta_az_deg
float
default:"0.5"
Azimuth resolution in degrees
band_defs
Optional[List[Dict]]
default:"None"
Custom band definitions. If None, uses DEFAULT_BANDS
progress_callback
callable
default:"None"
Optional callback(percent: int, msg: str) for progress updates
light_sampler
object
default:"None"
Optional light pollution sampler with get_radiance_utm(x, y) method
abort_check
callable
default:"None"
Optional callable() -> bool to check for abort requests
Returns: Tuple of (azimuths, bands, light_domes, light_peak_distances)

TileIndex

Indexes DEM tiles by bounding box in projected coordinates.

Constructor

TileIndex(
    tiles_dir: str,
    patterns: list = ["*.asc", "*.txt", "*.npy"],
    callback = None
)
tiles_dir
str
required
Directory containing DEM tiles
patterns
list
default:"['*.asc', '*.txt', '*.npy']"
File patterns to search for
callback
callable
default:"None"
Optional progress callback(current, total, message)

Attributes

tiles
List[Dict]
List of indexed tile dictionaries with ‘path’, ‘header’, and ‘bbox’
global_bbox
List[float]
Global bounding box [xmin, ymin, xmax, ymax] covering all tiles

Methods

find_tile
find_tile(x: float, y: float) -> Optional[Dict]
Returns the first tile containing the point (x, y).
get_overlapping_tiles
get_overlapping_tiles(
    cx: float,
    cy: float,
    radius: float
) -> List[Dict]
Returns all tiles within radius of (cx, cy), sorted by distance (closest first).
cx
float
required
Center X coordinate
cy
float
required
Center Y coordinate
radius
float
required
Search radius in meters

TileCache

LRU cache for loaded DEM grids with .npy binary caching on disk. Thread-safe.

Constructor

TileCache(capacity: int = 16)
capacity
int
default:"16"
Maximum number of tiles to keep in memory

Methods

load
load(tile_info: Dict) -> Tuple[Optional[np.ndarray], Optional[Dict]]
Loads a DEM tile, using cached .npy if available, otherwise parses the ASCII grid.
tile_info
Dict
required
Tile dictionary from TileIndex with ‘path’ and ‘header’ keys
Returns: Tuple of (data_array, header_dict) or (None, None) on error

DemSampler

Samples elevation from DEM tiles with bilinear interpolation.

Constructor

DemSampler(tile_index: TileIndex, tile_cache: TileCache)
tile_index
TileIndex
required
TileIndex instance for finding tiles
tile_cache
TileCache
required
TileCache instance for loading tiles

Methods

sample / get_elevation
sample(x: float, y: float) -> Optional[float]
get_elevation(x: float, y: float) -> Optional[float]
Samples elevation at (x, y) with bilinear interpolation. Both methods are equivalent.
x
float
required
X coordinate in projected CRS
y
float
required
Y coordinate in projected CRS
Returns: Elevation in meters or None if outside DEM coverage
transform_coordinates_inverse
transform_coordinates_inverse(
    x: float,
    y: float
) -> Tuple[float, float]
Converts UTM 31N coordinates back to Lat/Lon (WGS84).
x
float
required
UTM X coordinate
y
float
required
UTM Y coordinate
Returns: Tuple of (latitude, longitude) in degrees

Example Usage

from TerraLab.terrain.engine import (
    TileIndex,
    TileCache,
    DemSampler,
    HorizonBaker,
    generate_bands,
    bake_and_save
)

# Full pipeline approach
profile = bake_and_save(
    lat=42.1234,
    lon=1.5678,
    tiles_dir="/path/to/dem/tiles",
    output_path="horizon_profile.npz",
    radius=150_000,  # 150km
    step_m=50,
    resolution_deg=0.5,
    eye_height=1.7,
    band_defs=generate_bands(40)  # High quality
)

# Manual approach for more control
tile_index = TileIndex("/path/to/dem/tiles")
tile_cache = TileCache(capacity=100)
sampler = DemSampler(tile_index, tile_cache)
baker = HorizonBaker(sampler, eye_height=1.7)

# Transform coordinates to UTM
from pyproj import Transformer
transformer = Transformer.from_crs("EPSG:4326", "EPSG:25831", always_xy=True)
x_utm, y_utm = transformer.transform(lon, lat)

# Bake profile
azimuths, bands, light_domes, light_peak_distances = baker.bake(
    obs_x=x_utm,
    obs_y=y_utm,
    obs_h_ground=None,  # Auto-sample from DEM
    step_m=50,
    d_max=150_000,
    delta_az_deg=0.5,
    band_defs=generate_bands(40)
)

Build docs developers (and LLMs) love