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
  • Information Filters and SRIF
  • When to Use Information Filters
  • Information Filter Fundamentals
  • Standard Information Filter Algorithm
  • Square Root Information Filter (SRIF)
  • Extended Information Filter (EKIF)
  • Decorrelated Measurement Processing
  • Comparison: Information Filter vs Kalman Filter
  • Weak Initial Conditions (High Uncertainty)
  • Practical Diagnostic Tools
  • Batch Information Filter (BIF)
  • Tuning Guidelines
  • Common Pitfalls
  • Advanced Kalman Filter Variants
  • When to Use Advanced KF Variants
  • Cubature Kalman Filter (CKF)
  • Sigma-Point Kalman Filters
  • Centered Difference Kalman Filter
  • Ensemble Kalman Filter (EnKF)
  • Comparison: Advanced KF Variants
  • Hybrid Approaches
  • Practical Diagnostics
  • Tuning Guidelines
  • Common Pitfalls
  • Custom Filter Implementation
  • Why Implement Custom Filters
  • Design Patterns: Base Class Architecture
  • 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

Tracking & Association

  • Assignment & Data Association
  • Particle Filters & Non-Gaussian Estimation
  • Smoothing Algorithms & Offline Estimation
  • Data Structures & Containers

Domain-Specific

  • Coordinate Systems Deep Dive
  • Astronomical & Celestial Mechanics
  • Atmospheric Modeling with NRLMSISE-00
  • Navigation & Inertial Measurement Systems
  • Signal Processing Fundamentals

Performance & Advanced

  • GPU Acceleration Guide
  • Performance Optimization Guide

Reference & Learning

  • Troubleshooting Guide
  • MATLAB to Python Migration Guide
  • Gap Analysis: Python vs MATLAB TCL
  • 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
        • Map Projections
        • 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, datum conversions, and map projections.

Overview

Geodesy provides the mathematical foundation for navigation:

  • Geodetic datums: Earth ellipsoid models (WGS84)

  • Distance calculations: Vincenty, Haversine methods

  • Map projections: UTM, Mercator, Lambert Conformal

  • 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

Rhumb Lines
  • Constant bearing paths

  • Longer than great circles

  • Easier navigation

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

Map Projections

UTM (Universal Transverse Mercator)
  • Low distortion in zones

  • Standard military/civilian use

Mercator
  • Conformal (preserves angles)

  • Used for marine navigation

Lambert Conformal Conic
  • Low distortion for mid-latitudes

  • Used for aeronautical charts

Code Highlights

The example demonstrates:

  • geodetic_distance_vincenty() for accurate distances

  • geodetic_direct() for point from bearing/distance

  • utm_to_geodetic() and geodetic_to_utm()

  • Map projection functions

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

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