Terrain Modeling
This example demonstrates the pytcl.terrain module capabilities, including digital elevation model (DEM) creation, synthetic terrain generation, and terrain analysis.
Overview
Terrain modeling is essential for:
Navigation: Terrain-aided navigation and TERCOM
Simulation: Realistic environment modeling
Line-of-sight: Radio propagation and visibility
Mission planning: Route optimization
Digital Elevation Models
- Flat DEM
Constant elevation surface
Useful for testing and baseline comparisons
Created with
create_flat_dem()
- Synthetic Terrain
Procedurally generated terrain
Controllable parameters (amplitude, wavelength)
Useful for simulation and testing
Created with
create_synthetic_terrain()
DEM Properties
- Grid Structure
Regular lat/lon grid
Specified resolution in arcseconds
Elevation values at each grid point
- Coordinate System
Geographic coordinates (lat, lon)
Elevation in meters above reference
- Analysis Outputs
Min, max, mean elevation
Standard deviation
Slope and aspect maps
Terrain Analysis
- Elevation Statistics
Distribution of elevation values
Terrain roughness metrics
Histogram analysis
- Slope Computation
Gradient magnitude at each point
Degrees from horizontal
Important for mobility analysis
- Horizon Computation
Visible horizon from observer position
Accounts for terrain obstruction
Essential for line-of-sight analysis
Applications
- Terrain-Aided Navigation
Match measured terrain to DEM
Position fix without GPS
Submarine and aircraft navigation
- Viewshed Analysis
Determine visible area from point
Radar coverage planning
Communication link analysis
- Route Planning
Avoid steep terrain
Minimize exposure
Optimize fuel consumption
Terrain-Aided Navigation: Vehicle trajectories can be matched against terrain models for position updates without GPS.
Code Highlights
The example demonstrates:
Flat DEM creation with
create_flat_dem()Synthetic terrain with
create_synthetic_terrain()Terrain statistics (min, max, mean, std)
Slope computation using gradients
Horizon computation with
compute_horizon()
Source Code
1"""Terrain module demonstration with DEM and elevation models.
2
3This example demonstrates the pytcl.terrain module capabilities, including
4digital elevation model (DEM) creation, synthetic terrain generation, terrain
5analysis, and viewshed/line-of-sight computations.
6
7Functions demonstrated:
8- create_flat_dem(): Create flat digital elevation models
9- create_synthetic_terrain(): Generate synthetic terrain with hills/valleys
10- compute_horizon(): Calculate visible horizon from observer position
11"""
12
13import os
14from pathlib import Path
15
16import numpy as np
17import plotly.graph_objects as go
18from plotly.subplots import make_subplots
19
20from pytcl.terrain import compute_horizon, create_flat_dem, create_synthetic_terrain
21
22# Controls for visualization
23SHOW_PLOTS = os.environ.get("PYTCL_SHOW_PLOTS", "1") != "0"
24SKIP_VISUALIZATIONS = True # Skip visualizations for fast execution
25
26
27def demo_flat_dem() -> None:
28 """Demonstrate flat DEM creation."""
29 print("\n" + "=" * 60)
30 print("Flat Digital Elevation Model (DEM)")
31 print("=" * 60)
32
33 # Create a flat DEM
34 dem = create_flat_dem(
35 lat_min=np.radians(-1),
36 lat_max=np.radians(1),
37 lon_min=np.radians(-1),
38 lon_max=np.radians(1),
39 elevation=1000.0,
40 resolution_arcsec=60,
41 )
42
43 print(f"\nFlat DEM created:")
44 print(
45 f" Latitude range: [{np.degrees(dem.lat_min):.2f}°, {np.degrees(dem.lat_max):.2f}°]"
46 )
47 print(
48 f" Longitude range: [{np.degrees(dem.lon_min):.2f}°, {np.degrees(dem.lon_max):.2f}°]"
49 )
50 print(f" Shape: {dem.data.shape}")
51 print(f" Elevation: {dem.data.min():.1f} to {dem.data.max():.1f} m")
52 print(
53 f" Grid spacing: {np.degrees(dem.d_lat):.6f}° lat, {np.degrees(dem.d_lon):.6f}° lon"
54 )
55
56 # Visualization (fast heatmap with downsampling for large grids)
57 if not SKIP_VISUALIZATIONS:
58 # Downsample for faster visualization (keep full resolution data for computations)
59 max_size = 500
60 stride = max(1, max(dem.data.shape) // max_size)
61 z_display = dem.data[::stride, ::stride]
62
63 fig = go.Figure(
64 data=go.Heatmap(
65 z=z_display,
66 colorscale="Viridis",
67 name="Elevation",
68 colorbar=dict(title="Elevation (m)"),
69 )
70 )
71
72 fig.update_layout(
73 title="Flat Digital Elevation Model",
74 xaxis_title="Longitude index",
75 yaxis_title="Latitude index",
76 height=500,
77 )
78
79 if SHOW_PLOTS:
80 fig.show()
81 else:
82 fig.write_html(
83 str(OUTPUT_DIR / "terrain_demo.html"),
84 include_plotlyjs="cdn",
85 div_id="terrain_demo",
86 )
87
88
89def demo_synthetic_terrain() -> None:
90 """Demonstrate synthetic terrain generation."""
91 print("\n" + "=" * 60)
92 print("Synthetic Terrain Generation")
93 print("=" * 60)
94
95 # Create synthetic terrain with hills
96 dem = create_synthetic_terrain(
97 lat_min=np.radians(-5),
98 lat_max=np.radians(5),
99 lon_min=np.radians(-5),
100 lon_max=np.radians(5),
101 base_elevation=500,
102 amplitude=800,
103 wavelength_km=50,
104 resolution_arcsec=120,
105 seed=42,
106 )
107
108 print(f"\nSynthetic DEM created:")
109 print(
110 f" Latitude range: [{np.degrees(dem.lat_min):.2f}°, {np.degrees(dem.lat_max):.2f}°]"
111 )
112 print(
113 f" Longitude range: [{np.degrees(dem.lon_min):.2f}°, {np.degrees(dem.lon_max):.2f}°]"
114 )
115 print(f" Shape: {dem.data.shape}")
116 print(f" Min elevation: {dem.data.min():.1f} m")
117 print(f" Max elevation: {dem.data.max():.1f} m")
118 print(f" Mean elevation: {dem.data.mean():.1f} m")
119 print(f" Std deviation: {dem.data.std():.1f} m")
120 print(
121 f" Grid spacing: {np.degrees(dem.d_lat):.6f}° lat, {np.degrees(dem.d_lon):.6f}° lon"
122 )
123
124 # Visualization: 2D heatmap with downsampling (fast rendering)
125 if not SKIP_VISUALIZATIONS:
126 # Downsample for faster visualization
127 max_size = 500
128 stride = max(1, max(dem.data.shape) // max_size)
129 z_display = dem.data[::stride, ::stride]
130
131 fig = go.Figure(
132 data=go.Heatmap(
133 z=z_display,
134 colorscale="Earth",
135 name="Elevation",
136 colorbar=dict(title="Elevation (m)"),
137 )
138 )
139
140 fig.update_layout(
141 title="Synthetic Terrain with Synthetic Hills",
142 xaxis_title="Longitude index",
143 yaxis_title="Latitude index",
144 height=600,
145 )
146
147 if SHOW_PLOTS:
148 fig.show()
149 else:
150 fig.write_html(
151 str(OUTPUT_DIR / "terrain_demo.html"),
152 include_plotlyjs="cdn",
153 div_id="terrain_demo",
154 )
155
156
157def demo_terrain_analysis() -> None:
158 """Demonstrate terrain analysis and statistics."""
159 print("\n" + "=" * 60)
160 print("Terrain Analysis and Statistics")
161 print("=" * 60)
162
163 # Create two DEMs for comparison
164 flat_dem = create_flat_dem(
165 lat_min=np.radians(-2),
166 lat_max=np.radians(2),
167 lon_min=np.radians(-2),
168 lon_max=np.radians(2),
169 elevation=500.0,
170 resolution_arcsec=60,
171 )
172
173 synthetic_dem = create_synthetic_terrain(
174 lat_min=np.radians(-2),
175 lat_max=np.radians(2),
176 lon_min=np.radians(-2),
177 lon_max=np.radians(2),
178 base_elevation=500,
179 amplitude=400,
180 wavelength_km=30,
181 resolution_arcsec=60,
182 seed=123,
183 )
184
185 # Compute statistics
186 print(f"\nFlat DEM statistics:")
187 print(f" Min: {flat_dem.data.min():.1f} m")
188 print(f" Max: {flat_dem.data.max():.1f} m")
189 print(f" Mean: {flat_dem.data.mean():.1f} m")
190 print(f" Std Dev: {flat_dem.data.std():.1f} m")
191
192 print(f"\nSynthetic DEM statistics:")
193 print(f" Min: {synthetic_dem.data.min():.1f} m")
194 print(f" Max: {synthetic_dem.data.max():.1f} m")
195 print(f" Mean: {synthetic_dem.data.mean():.1f} m")
196 print(f" Std Dev: {synthetic_dem.data.std():.1f} m")
197
198 # Terrain slope. np.gradient returns meters of rise per *grid cell*, so it
199 # has to be divided by the ground spacing of a cell before it means
200 # anything -- without that the slope comes out near-vertical everywhere.
201 earth_radius = 6371000.0
202 mean_lat = 0.5 * (synthetic_dem.lat_min + synthetic_dem.lat_max)
203 dy_m = synthetic_dem.d_lat * earth_radius
204 dx_m = synthetic_dem.d_lon * earth_radius * np.cos(mean_lat)
205
206 grad_y, grad_x = np.gradient(synthetic_dem.data)
207 slope = np.degrees(np.arctan(np.hypot(grad_x / dx_m, grad_y / dy_m)))
208
209 print(f"\nGrid cell ground size: {dx_m:.0f} m east-west, {dy_m:.0f} m north-south")
210
211 print(f"\nTerrain slope analysis:")
212 print(f" Min slope: {slope.min():.2f}°")
213 print(f" Max slope: {slope.max():.2f}°")
214 print(f" Mean slope: {slope.mean():.2f}°")
215
216 # Visualization: Comparison histograms (skip for performance)
217 if not SKIP_VISUALIZATIONS:
218 fig = make_subplots(
219 rows=1,
220 cols=2,
221 subplot_titles=("Flat DEM Distribution", "Synthetic Terrain Distribution"),
222 )
223
224 fig.add_trace(
225 go.Histogram(
226 x=flat_dem.data.flatten(),
227 nbinsx=30,
228 name="Flat DEM",
229 marker_color="steelblue",
230 ),
231 row=1,
232 col=1,
233 )
234
235 fig.add_trace(
236 go.Histogram(
237 x=synthetic_dem.data.flatten(),
238 nbinsx=30,
239 name="Synthetic Terrain",
240 marker_color="coral",
241 ),
242 row=1,
243 col=2,
244 )
245
246 fig.update_xaxes(title_text="Elevation (m)", row=1, col=1)
247 fig.update_xaxes(title_text="Elevation (m)", row=1, col=2)
248 fig.update_yaxes(title_text="Count", row=1, col=1)
249 fig.update_layout(height=400, showlegend=True)
250
251 if SHOW_PLOTS:
252 fig.show()
253 else:
254 fig.write_html(
255 str(OUTPUT_DIR / "terrain_demo.html"),
256 include_plotlyjs="cdn",
257 div_id="terrain_demo",
258 )
259
260 # Visualization: Slope map with downsampling
261 max_size = 500
262 stride_slope = max(1, max(slope.shape) // max_size)
263 z_slope_display = slope[::stride_slope, ::stride_slope]
264
265 fig_slope = go.Figure(
266 data=go.Heatmap(
267 z=z_slope_display,
268 colorscale="Reds",
269 name="Slope",
270 colorbar=dict(title="Slope (°)"),
271 )
272 )
273
274 fig_slope.update_layout(
275 title="Terrain Slope Map",
276 xaxis_title="Longitude index",
277 yaxis_title="Latitude index",
278 height=500,
279 )
280
281 if SHOW_PLOTS:
282 fig_slope.show()
283
284
285def demo_horizon_computation() -> None:
286 """Demonstrate horizon computation."""
287 print("\n" + "=" * 60)
288 print("Horizon Computation")
289 print("=" * 60)
290
291 # A one-degree box of synthetic terrain. All angles are radians, per the
292 # library-wide convention -- passing degrees here would ask for a DEM
293 # spanning most of the globe.
294 lat_min, lat_max = np.radians(35.0), np.radians(36.0)
295 lon_min, lon_max = np.radians(-120.0), np.radians(-119.0)
296 dem = create_synthetic_terrain(
297 lat_min=lat_min,
298 lat_max=lat_max,
299 lon_min=lon_min,
300 lon_max=lon_max,
301 base_elevation=500,
302 amplitude=500,
303 wavelength_km=40,
304 resolution_arcsec=30,
305 seed=456,
306 )
307
308 print(f"\nDEM for horizon analysis:")
309 print(f" Shape: {dem.data.shape}")
310 print(f" Elevation range: {dem.data.min():.1f} to {dem.data.max():.1f} m")
311
312 # Observe from the middle of the box, 100 m above local ground level.
313 obs_lat = 0.5 * (lat_min + lat_max)
314 obs_lon = 0.5 * (lon_min + lon_max)
315 obs_height = 100.0
316
317 print(f"\nObserver position:")
318 print(f" Latitude: {np.degrees(obs_lat):.4f}°")
319 print(f" Longitude: {np.degrees(obs_lon):.4f}°")
320 print(f" Ground elevation: {dem.get_elevation(obs_lat, obs_lon).elevation:.1f} m")
321 print(f" Observer height above ground: {obs_height} m")
322
323 horizon = compute_horizon(
324 dem,
325 obs_lat,
326 obs_lon,
327 obs_height,
328 n_azimuths=72,
329 max_range=40000.0,
330 )
331
332 # compute_horizon returns one HorizonPoint per azimuth.
333 elevations = np.array([p.elevation_angle for p in horizon])
334 distances = np.array([p.distance for p in horizon])
335 azimuths = np.array([p.azimuth for p in horizon])
336
337 print(f"\nHorizon profile over {len(horizon)} azimuths:")
338 print(
339 f" Elevation angle: {np.degrees(elevations.min()):+.2f}° to "
340 f"{np.degrees(elevations.max()):+.2f}°"
341 )
342 print(
343 f" Horizon distance: {distances.min() / 1e3:.1f} to "
344 f"{distances.max() / 1e3:.1f} km"
345 )
346
347 highest = int(np.argmax(elevations))
348 print(
349 f" Highest horizon at azimuth {np.degrees(azimuths[highest]):.1f}° "
350 f"({np.degrees(elevations[highest]):+.2f}°, "
351 f"{distances[highest] / 1e3:.1f} km away)"
352 )
353
354
355def main() -> None:
356 """Run all demonstrations."""
357 print("\n" + "=" * 60)
358 print("Terrain Module Demonstration")
359 print("=" * 60)
360
361 demo_flat_dem()
362 demo_synthetic_terrain()
363 demo_terrain_analysis()
364 demo_horizon_computation()
365
366 print("\n" + "=" * 60)
367 print("Demonstration Complete")
368 print("=" * 60)
369
370
371OUTPUT_DIR = Path("docs/_static/images/examples")
372OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
373
374if __name__ == "__main__":
375 main()
Running the Example
python examples/terrain_demo.py
See Also
INS/GNSS Navigation - Navigation applications
Coordinate Systems - Coordinate transformations
Advanced Reference Frames - Reference frames