Tracker Component Library

Start Here

  • Getting Started
  • Library Architecture
  • API Navigation Guide
  • Common Use Cases & Recipes

Filtering & Estimation

  • Kalman Filter Tuning Guide
  • Constrained State Estimation
  • Hybrid Linear/Nonlinear Filtering with RBPF
  • Adaptive Filtering
  • When to Use Adaptive Filtering
  • Divergence Detection Techniques
  • Noise Covariance Estimation
  • Adaptive Kalman Filtering
  • Least Mean Squares (LMS) Adaptation
  • Recursive Least Squares (RLS) Adaptation
  • Practical Adaptive Filter Systems
  • Diagnostic Tools
  • Tuning Guidelines
  • Common Pitfalls
  • See Also
  • Information Filters and SRIF
  • Advanced Kalman Filter Variants
  • When to Use Advanced KF Variants
  • Cubature Kalman Filter (CKF)
  • Sigma-Point Kalman Filters
  • Central Difference (Numerical-Jacobian) Filtering
  • Ensemble Kalman Filter (EnKF)
  • Comparison: Advanced KF Variants
  • Mixing Variants
  • Practical Diagnostics
  • Tuning Guidelines
  • Common Pitfalls
  • See Also
  • Custom Filter Implementation
  • Why Implement Custom Filters
  • Design Patterns: Class-Based Wrappers
  • Example 1: Custom Adaptive Constant Velocity Filter
  • Example 2: Wrapping External C++ Filter
  • Integration with TCL Components
  • Testing Custom Filters
  • Performance Optimization
  • Practical Workflow: Algorithm to Integration
  • Documentation and Type Hints
  • Common Pitfalls and Solutions
  • See Also

Tracking & Association

  • Assignment & Data Association
  • Particle Filters & Non-Gaussian Estimation
  • Smoothing Algorithms & Offline Estimation
  • Data Structures & Containers
  • Results I/O
  • Typed Configs and Sessions

Domain-Specific

  • Coordinate Systems Deep Dive
  • Astronomical & Celestial Mechanics
  • Thermosphere Density Modeling
  • Navigation & Inertial Measurement Systems
  • Signal Processing Fundamentals

Performance & Advanced

  • GPU Acceleration Guide
  • Performance Optimization Guide
  • Diagnostics Guide

Reference & Learning

  • Troubleshooting Guide
  • Migrating from v1.x to v2.0.0
  • MATLAB to Python Migration Guide
  • MATLAB TCL parity inventory
  • MATLAB-to-pytcl migration map
  • Development Roadmap
  • User Guide
  • Tutorials
  • Interactive Notebooks
  • Examples
    • Filtering & Estimation
    • Multi-Target Tracking
    • Clustering & Data Structures
    • Signal Processing & Transforms
    • Coordinate Systems & Navigation
      • Coordinate Systems
      • Coordinate Visualization
      • INS/GNSS Navigation
      • Navigation and Geodesy
        • Overview
        • Geodetic Calculations
        • Code Highlights
        • Source Code
        • Running the Example
        • See Also
    • Orbital & Celestial Mechanics
    • Geophysical & Atmospheric
    • Dynamic Models
    • Running Examples
    • Requirements
    • Generating Documentation Images
  • API Reference
Tracker Component Library
  • Examples
  • Coordinate Systems & Navigation
  • Navigation and Geodesy
  • View page source

Navigation and Geodesy

This example demonstrates geodetic calculations, coordinate conversions, and great-circle navigation.

Overview

Geodesy provides the mathematical foundation for navigation:

  • Geodetic datums: Earth ellipsoid models (WGS84)

  • Distance calculations: Vincenty, Haversine methods

  • Local frames: ECEF and ENU conversions

  • Great circles: Shortest paths on Earth

Geodetic Calculations

Vincenty’s Formulae
  • High accuracy (< 0.5mm)

  • Works for all distances

  • Handles antipodal points

Haversine Formula
  • Simpler calculation

  • Good for short distances

  • Assumes spherical Earth

Earth Ellipsoid: The WGS84 reference ellipsoid with coordinate frames at various locations.

Code Highlights

The example demonstrates:

  • inverse_geodetic() (Vincenty) for accurate distances and azimuths

  • direct_geodetic() for the point reached from a bearing and distance

  • haversine_distance() for quick spherical-Earth distances

  • geodetic_to_ecef()/ecef_to_geodetic() and ecef_to_enu()/ enu_to_ecef() frame conversions

  • Multi-waypoint route planning and sensor coverage analysis

Source Code

  1"""
  2Navigation and Geodesy Example.
  3
  4This example demonstrates:
  51. Geodetic coordinate conversions (WGS84)
  62. Local tangent plane transformations (ENU/NED)
  73. Geodetic distance calculations
  84. Multi-waypoint navigation
  95. Sensor placement and coverage analysis
 10
 11Run with: python examples/navigation_geodesy.py
 12"""
 13
 14import sys
 15from pathlib import Path
 16
 17sys.path.insert(0, str(Path(__file__).parent.parent))
 18
 19# Output directory for generated plots
 20OUTPUT_DIR = Path(__file__).parent.parent / "docs" / "_static" / "images" / "examples"
 21OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
 22
 23import os
 24
 25import numpy as np  # noqa: E402
 26import plotly.graph_objects as go  # noqa: E402
 27
 28from pytcl.navigation import (
 29    direct_geodetic,
 30    ecef_to_enu,
 31    ecef_to_geodetic,
 32    enu_to_ecef,
 33    geodetic_to_ecef,
 34    haversine_distance,
 35    inverse_geodetic,
 36)
 37
 38SHOW_PLOTS = os.environ.get("PYTCL_SHOW_PLOTS", "1") != "0"
 39
 40
 41def geodetic_basics_demo() -> None:
 42    """Demonstrate basic geodetic coordinate conversions."""
 43    print("=" * 60)
 44    print("1. GEODETIC COORDINATE CONVERSIONS")
 45    print("=" * 60)
 46
 47    # Key locations
 48    locations = {
 49        "Washington DC": (38.9072, -77.0369, 0.0),
 50        "New York City": (40.7128, -74.0060, 0.0),
 51        "Los Angeles": (34.0522, -118.2437, 0.0),
 52        "GPS Satellite (MEO)": (0.0, -75.0, 20200000.0),  # ~20,200 km altitude
 53    }
 54
 55    print("\nGeodetic to ECEF conversions:")
 56    print("-" * 60)
 57
 58    for name, (lat_deg, lon_deg, alt) in locations.items():
 59        lat = np.radians(lat_deg)
 60        lon = np.radians(lon_deg)
 61
 62        # Convert to ECEF
 63        ecef = geodetic_to_ecef(lat, lon, alt)
 64
 65        print(f"\n{name}:")
 66        print(f"  Geodetic: {lat_deg:.4f}N, {lon_deg:.4f}E, {alt:.0f} m")
 67        print(
 68            f"  ECEF: X={ecef[0] / 1000:.1f} km, Y={ecef[1] / 1000:.1f} km, "
 69            f"Z={ecef[2] / 1000:.1f} km"
 70        )
 71
 72        # Convert back
 73        lat_r, lon_r, alt_r = ecef_to_geodetic(ecef[0], ecef[1], ecef[2])
 74        print(
 75            f"  Roundtrip: {np.degrees(lat_r):.4f}N, {np.degrees(lon_r):.4f}E, "
 76            f"{alt_r:.0f} m"
 77        )
 78
 79
 80def distance_calculations_demo() -> None:
 81    """Demonstrate geodetic distance calculations."""
 82    print("\n" + "=" * 60)
 83    print("2. GEODETIC DISTANCE CALCULATIONS")
 84    print("=" * 60)
 85
 86    # City pairs for distance calculation
 87    city_pairs = [
 88        ("Washington DC", (38.9072, -77.0369), "New York City", (40.7128, -74.0060)),
 89        ("Washington DC", (38.9072, -77.0369), "Los Angeles", (34.0522, -118.2437)),
 90        ("New York City", (40.7128, -74.0060), "London", (51.5074, -0.1278)),
 91        ("Los Angeles", (34.0522, -118.2437), "Tokyo", (35.6762, 139.6503)),
 92    ]
 93
 94    print("\nGreat circle distances:")
 95    print("-" * 60)
 96
 97    for city1, (lat1, lon1), city2, (lat2, lon2) in city_pairs:
 98        lat1_rad = np.radians(lat1)
 99        lon1_rad = np.radians(lon1)
100        lat2_rad = np.radians(lat2)
101        lon2_rad = np.radians(lon2)
102
103        # Haversine (approximate, fast)
104        dist_haversine = haversine_distance(lat1_rad, lon1_rad, lat2_rad, lon2_rad)
105
106        # Inverse geodetic (accurate)
107        dist_geodetic, az_fwd, az_back = inverse_geodetic(
108            lat1_rad, lon1_rad, lat2_rad, lon2_rad
109        )
110
111        print(f"\n{city1} -> {city2}:")
112        print(f"  Haversine distance: {dist_haversine / 1000:.1f} km")
113        print(f"  Geodetic distance:  {dist_geodetic / 1000:.1f} km")
114        print(f"  Forward azimuth:    {np.degrees(az_fwd):.1f} deg")
115        print(f"  Back azimuth:       {np.degrees(az_back):.1f} deg")
116
117
118def local_frame_demo() -> None:
119    """Demonstrate local tangent plane (ENU) conversions."""
120    print("\n" + "=" * 60)
121    print("3. LOCAL TANGENT PLANE (ENU) FRAME")
122    print("=" * 60)
123
124    # Reference point: Washington DC
125    ref_lat = np.radians(38.9072)
126    ref_lon = np.radians(-77.0369)
127    ref_alt = 0.0
128
129    print("\nReference point: Washington DC")
130    print(f"  Lat: {np.degrees(ref_lat):.4f} deg")
131    print(f"  Lon: {np.degrees(ref_lon):.4f} deg")
132
133    # Define points relative to reference in ENU
134    enu_points = {
135        "10 km North": np.array([0.0, 10000.0, 0.0]),
136        "10 km East": np.array([10000.0, 0.0, 0.0]),
137        "10 km NE, 500m Up": np.array([7071.0, 7071.0, 500.0]),
138        "Aircraft overhead (10 km)": np.array([0.0, 0.0, 10000.0]),
139    }
140
141    print("\nENU to Geodetic conversions:")
142    print("-" * 60)
143
144    for name, enu in enu_points.items():
145        # Convert ENU to ECEF (separate components)
146        x, y, z = enu_to_ecef(enu[0], enu[1], enu[2], ref_lat, ref_lon, ref_alt)
147
148        # Convert ECEF to geodetic
149        lat, lon, alt = ecef_to_geodetic(x, y, z)
150
151        print(f"\n{name}:")
152        print(f"  ENU: E={enu[0]:.0f} m, N={enu[1]:.0f} m, U={enu[2]:.0f} m")
153        print(
154            f"  Geodetic: {np.degrees(lat):.4f}N, {np.degrees(lon):.4f}E, {alt:.0f} m"
155        )
156
157        # Verify roundtrip
158        e_r, n_r, u_r = ecef_to_enu(x, y, z, ref_lat, ref_lon, ref_alt)
159        enu_recovered = np.array([e_r, n_r, u_r])
160        error = np.linalg.norm(enu - enu_recovered)
161        print(f"  Roundtrip error: {error:.2e} m")
162
163
164def waypoint_navigation_demo() -> None:
165    """Demonstrate waypoint-to-waypoint navigation."""
166    print("\n" + "=" * 60)
167    print("4. WAYPOINT NAVIGATION")
168    print("=" * 60)
169
170    # Define a flight path with waypoints
171    waypoints = [
172        ("DCA (Reagan Airport)", 38.8521, -77.0377),
173        ("Waypoint 1", 39.5, -76.5),
174        ("Waypoint 2", 40.0, -75.5),
175        ("JFK Airport", 40.6413, -73.7781),
176    ]
177
178    print("\nFlight path waypoints:")
179    print("-" * 60)
180
181    total_distance = 0.0
182    for i, (name, lat, lon) in enumerate(waypoints):
183        print(f"\n{i + 1}. {name}: {lat:.4f}N, {lon:.4f}E")
184
185        if i > 0:
186            # Calculate leg distance and heading
187            prev_name, prev_lat, prev_lon = waypoints[i - 1]
188            lat1_rad = np.radians(prev_lat)
189            lon1_rad = np.radians(prev_lon)
190            lat2_rad = np.radians(lat)
191            lon2_rad = np.radians(lon)
192
193            dist, az_fwd, _ = inverse_geodetic(lat1_rad, lon1_rad, lat2_rad, lon2_rad)
194            total_distance += dist
195
196            print(f"   From {prev_name}:")
197            heading = np.degrees(az_fwd)
198            print(f"   Distance: {dist / 1000:.1f} km, Heading: {heading:.1f} deg")
199
200    print(f"\nTotal flight distance: {total_distance / 1000:.1f} km")
201
202    # Compute intermediate points along each leg using direct geodetic
203    print("\nIntermediate points along first leg (every 10 km):")
204    print("-" * 60)
205
206    lat1 = np.radians(waypoints[0][1])
207    lon1 = np.radians(waypoints[0][2])
208    lat2 = np.radians(waypoints[1][1])
209    lon2 = np.radians(waypoints[1][2])
210
211    leg_dist, az_fwd, _ = inverse_geodetic(lat1, lon1, lat2, lon2)
212
213    for d in np.arange(0, leg_dist, 10000):  # Every 10 km
214        lat_int, lon_int, _ = direct_geodetic(lat1, lon1, az_fwd, d)
215        print(
216            f"  {d / 1000:.0f} km: {np.degrees(lat_int):.4f}N, "
217            f"{np.degrees(lon_int):.4f}E"
218        )
219
220
221def sensor_coverage_demo() -> None:
222    """Demonstrate sensor placement and coverage analysis."""
223    print("\n" + "=" * 60)
224    print("5. SENSOR COVERAGE ANALYSIS")
225    print("=" * 60)
226
227    # Radar sensor location (Washington DC)
228    sensor_alt = 50.0  # 50m tower
229
230    # Sensor parameters
231    max_range = 100000.0  # 100 km
232    min_elevation = np.radians(2.0)  # 2 degree minimum elevation
233
234    print("\nRadar sensor location: Washington DC")
235    print(f"  Height: {sensor_alt} m")
236    print(f"  Max range: {max_range / 1000:.0f} km")
237    print(f"  Min elevation: {np.degrees(min_elevation):.1f} deg")
238
239    # Calculate coverage at different altitudes
240    print("\nCoverage radius at different target altitudes:")
241    print("-" * 60)
242
243    target_altitudes_m = [alt * 0.3048 for alt in [1000, 5000, 10000, 20000, 40000]]
244
245    for alt_m in target_altitudes_m:
246        # Height difference
247        delta_h = alt_m - sensor_alt
248
249        # Maximum slant range is either max_range or limited by min elevation
250        # At min elevation, slant range r = delta_h / sin(min_el)
251        range_elev_limited = delta_h / np.sin(min_elevation) if delta_h > 0 else 0
252        effective_range = min(max_range, range_elev_limited)
253
254        # Ground range
255        if effective_range > 0:
256            ground_range = np.sqrt(effective_range**2 - delta_h**2)
257        else:
258            ground_range = 0
259
260        print(f"  Target at {alt_m:.0f} m ({alt_m / 0.3048:.0f} ft):")
261        print(f"    Effective slant range: {effective_range / 1000:.1f} km")
262        print(f"    Ground coverage radius: {ground_range / 1000:.1f} km")
263
264    # Check if specific targets are in coverage
265    print("\nTarget detection check:")
266    print("-" * 60)
267
268    targets = [
269        ("Aircraft 50km E, 10km alt", 50000.0, 0.0, 10000.0),
270        ("Aircraft 80km NE, 5km alt", 56569.0, 56569.0, 5000.0),
271        ("Low flyer 30km N, 100m alt", 0.0, 30000.0, 100.0),
272        ("High alt 120km W, 20km alt", -120000.0, 0.0, 20000.0),
273    ]
274
275    for name, e, n, u in targets:
276        enu = np.array([e, n, u - sensor_alt])
277        slant_range = np.linalg.norm(enu)
278        elevation = np.arcsin(enu[2] / slant_range) if slant_range > 0 else 0
279
280        in_range = slant_range <= max_range
281        above_horizon = elevation >= min_elevation
282        detectable = in_range and above_horizon
283
284        status = "DETECTABLE" if detectable else "NOT DETECTABLE"
285        reason = []
286        if not in_range:
287            reason.append(
288                f"range {slant_range / 1000:.1f} km > {max_range / 1000:.0f} km"
289            )
290        if not above_horizon:
291            reason.append(
292                f"elev {np.degrees(elevation):.1f} deg < "
293                f"{np.degrees(min_elevation):.1f} deg"
294            )
295
296        print(f"\n  {name}:")
297        print(
298            f"    Range: {slant_range / 1000:.1f} km, "
299            f"Elevation: {np.degrees(elevation):.1f} deg"
300        )
301        print(f"    Status: {status}")
302        if reason:
303            print(f"    Reason: {', '.join(reason)}")
304
305
306def plot_coverage_map() -> None:
307    """Create an interactive coverage map."""
308    print("\n" + "=" * 60)
309    print("6. GENERATING COVERAGE MAP")
310    print("=" * 60)
311
312    # Sensor location
313    sensor_lat = np.radians(38.9072)
314    sensor_lon = np.radians(-77.0369)
315    sensor_alt = 50.0
316    max_range = 100000.0
317
318    # Generate coverage circle points
319    n_points = 72
320    azimuths = np.linspace(0, 2 * np.pi, n_points)
321
322    # Coverage at different altitudes
323    altitudes = [1000, 5000, 10000]  # meters
324    colors = ["green", "blue", "red"]
325
326    fig = go.Figure()
327
328    # Add sensor location
329    fig.add_trace(
330        go.Scattergeo(
331            lon=[np.degrees(sensor_lon)],
332            lat=[np.degrees(sensor_lat)],
333            mode="markers",
334            marker=dict(size=15, color="black", symbol="triangle-up"),
335            name="Radar Sensor",
336        )
337    )
338
339    # Add coverage circles for each altitude
340    for alt, color in zip(altitudes, colors):
341        # Calculate ground range for this altitude
342        delta_h = alt - sensor_alt
343        min_elev = np.radians(2.0)
344        range_elev_limited = delta_h / np.sin(min_elev)
345        effective_range = min(max_range, range_elev_limited)
346        ground_range = np.sqrt(max(0, effective_range**2 - delta_h**2))
347
348        # Generate circle points
349        lats = []
350        lons = []
351        for az in azimuths:
352            # direct_geodetic also returns the back azimuth at the destination.
353            lat, lon, _ = direct_geodetic(sensor_lat, sensor_lon, az, ground_range)
354            lats.append(np.degrees(lat))
355            lons.append(np.degrees(lon))
356
357        # Close the circle
358        lats.append(lats[0])
359        lons.append(lons[0])
360
361        fig.add_trace(
362            go.Scattergeo(
363                lon=lons,
364                lat=lats,
365                mode="lines",
366                line=dict(width=2, color=color),
367                name=f"Coverage at {alt}m ({alt * 3.28084:.0f}ft)",
368            )
369        )
370
371    fig.update_layout(
372        title="Radar Coverage Map (Washington DC)",
373        geo=dict(
374            scope="usa",
375            center=dict(lat=np.degrees(sensor_lat), lon=np.degrees(sensor_lon)),
376            projection_scale=5,
377            showland=True,
378            landcolor="rgb(243, 243, 243)",
379            countrycolor="rgb(204, 204, 204)",
380        ),
381        width=900,
382        height=700,
383    )
384
385    fig.write_html(
386        str(OUTPUT_DIR / "navigation_coverage_map.html"),
387        include_plotlyjs="cdn",
388        div_id="navigation_coverage_map",
389    )
390    print("\nInteractive coverage map saved to navigation_coverage_map.html")
391    if SHOW_PLOTS:
392        fig.show()
393
394
395def main() -> None:
396    """Run navigation and geodesy demonstrations."""
397    print("\nNavigation and Geodesy Examples")
398    print("=" * 60)
399    print("Demonstrating pytcl navigation capabilities")
400
401    geodetic_basics_demo()
402    distance_calculations_demo()
403    local_frame_demo()
404    waypoint_navigation_demo()
405    sensor_coverage_demo()
406
407    plot_coverage_map()
408
409    print("\n" + "=" * 60)
410    print("Done!")
411    print("=" * 60)
412
413
414if __name__ == "__main__":
415    main()

Running the Example

python examples/navigation_geodesy.py

See Also

  • INS/GNSS Navigation - INS/GNSS integration

  • Coordinate Systems - Coordinate conversions

Previous Next

© Copyright 2024-2026, nrl-tracker contributors; original MATLAB library by the U.S. Naval Research Laboratory (public domain).

Built with Sphinx using a theme provided by Read the Docs.