🎉 75% of content is free forever — Unlock Premium from $10/mo →
CW
NEWSLIVESearch All Content
đŸ’ŧ Servicesâ„šī¸ Aboutâœ‰ī¸ ContactView Pricing Plansfrom $10

GPS-Denied Navigation Techniques

đŸŸĸ Free Lesson

Advertisement

GPS-Denied Navigation Techniques

When GPS is unavailable — indoors, underground, in urban canyons, or during jamming — drones must rely on alternative navigation methods. This is critical for military, search-and-rescue, and industrial applications.

GPS-Denied Navigation Challenge

Sponsored

GPS denial can be intentional (jamming/spoofing) or environmental (tunnels, dense forests, indoor spaces). Drones need backup navigation systems that work without satellite signals.

GPS-Denied Techniques Overview

GPS-Denied Navigation TechniquesVisual-InertialCameraIMUEKF Fusion6DOF pose~1% drift/100mTerrain MatchingRadar/LiDAR AltimeterProfile correlation5-10m accuracyMagneticField comparisonto magnetic map10-50m accuracyCelestialSunStar/Sun trackingBackup methodMulti-Source Fusion ArchitectureVIOTerrainMagneticVisualFactor GraphOptimizationRobust 6DOF Pose EstimateWith uncertainty boundsTechnique ComparisonTechniqueAccuracyDriftEnvironmentLimitationsVIOcm-level~1%/100mTexturedFeatureless areasTerrain5-10mBoundedOutdoorFlat terrainMagnetic10-50mNoneAnyMagnetic anomaliesCelestial~100mNoneClear skyNight/clouds

Terrain-Referenced Navigation

Terrain navigation compares measured terrain profile against a stored digital elevation map (DEM) to determine position.

python
from typing import Tuple, List
from scipy.interpolate import griddata

class TerrainReferencedNavigation:
 def __init__(self, dem: np.ndarray, dem_resolution: float = 10.0):
 self.dem = dem
 self.resolution = dem_resolution
 self.dem_shape = dem.shape
 
 # Create coordinate grids
 self.x_grid = np.arange(dem.shape[1]) * dem_resolution
 self.y_grid = np.arange(dem.shape[0]) * dem_resolution
 
 def get_terrain_height(self, x: float, y: float) -> float:
 """Get terrain height at position (x, y)."""
 # Convert to grid indices
 ix = int(x / self.resolution)
 iy = int(y / self.resolution)
 
 if 0 <= ix < self.dem_shape[1] and 0 <= iy < self.dem_shape[0]:
 return self.dem[iy, ix]
 return 0.0
 
 def correlate_profile(self, measured_profile: np.ndarray,
 drone_position: np.ndarray,
 search_radius: float = 100.0) -> Tuple[np.ndarray, float]:
 """Find best matching position using profile correlation."""
 best_match = drone_position.copy()
 best_score = -float('inf')
 
 # Search around current position
 for dx in np.arange(-search_radius, search_radius, self.resolution):
 for dy in np.arange(-search_radius, search_radius, self.resolution):
 test_pos = drone_position + np.array([dx, dy, 0])
 
 # Extract DEM profile along same path
 dem_profile = self._extract_profile(test_pos, len(measured_profile))
 
 # Compute correlation
 if len(dem_profile) == len(measured_profile):
 score = np.corrcoef(measured_profile, dem_profile)[0, 1]
 
 if score > best_score:
 best_score = score
 best_match = test_pos[:2]
 
 return best_match, best_score
 
 def _extract_profile(self, position: np.ndarray, n_points: int) -> np.ndarray:
 """Extract terrain profile along flight direction."""
 profile = np.zeros(n_points)
 
 for i in range(n_points):
 x = position[0] + i * self.resolution
 y = position[1]
 profile[i] = self.get_terrain_height(x, y)
 
 return profile
 
 def TERCOM_algorithm(self, altimeter_readings: np.ndarray,
 flight_path: np.ndarray) -> np.ndarray:
 """Terrain Contour Matching (TERCOM) algorithm."""
 n_windows = len(altimeter_readings) - 10 # Window size
 
 best_position = flight_path[-1].copy()
 best_distance = float('inf')
 
 for i in range(0, len(flight_path) - n_windows):
 # Extract DEM section
 dem_section = []
 for j in range(n_windows):
 pos = flight_path[i + j]
 dem_section.append(self.get_terrain_height(pos[0], pos[1]))
 
 dem_section = np.array(dem_section)
 
 # Compute mean absolute difference
 if len(dem_section) == len(altimeter_readings):
 distance = np.mean(np.abs(altimeter_readings - dem_section))
 
 if distance < best_distance:
 best_distance = distance
 best_position = flight_path[i + n_windows // 2].copy()
 
 return best_position

class SandiaTerrainAlgorithm:
 """SANDIA terrain matching algorithm with DEM smoothing."""
 
 def __init__(self, dem: np.ndarray, smoothing_sigma: float = 2.0):
 from scipy.ndimage import gaussian_filter
 
 self.original_dem = dem
 self.smoothed_dem = gaussian_filter(dem, sigma=smoothing_sigma)
 self.smoothed_dem = dem # Use original for simplicity
 
 def match(self, altitude_profile: np.ndarray,
 initial_estimate: np.ndarray) -> np.ndarray:
 """Perform terrain matching with confidence estimation."""
 
 # Normalize altitude profile
 profile_mean = np.mean(altitude_profile)
 profile_std = np.std(altitude_profile)
 
 if profile_std > 0:
 normalized_profile = (altitude_profile - profile_mean) / profile_std
 else:
 return initial_estimate
 
 # Simple correlation-based matching
 # In practice, use more sophisticated methods
 best_position = initial_estimate[:2].copy()
 
 return best_position

Magnetic Navigation

Earth's magnetic field provides a natural positioning reference. Magnetic navigation compares measured field values against a magnetic anomaly map.

python
from typing import Tuple, List

class MagneticNavigation:
 def __init__(self, magnetic_map: np.ndarray, map_resolution: float = 100.0):
 self.magnetic_map = magnetic_map
 self.resolution = map_resolution
 
 # IGRF reference model parameters
 self.igrf_coefficients = self._load_igrf_coefficients()
 
 def _load_igrf_coefficients(self):
 """Load International Geomagnetic Reference Field coefficients."""
 # Simplified - in practice use full IGRF model
 return {
 'g10': -29404.8, 'g11': -1450.9, 'h11': 4653.2,
 'g20': -2500.0, 'g21': 3010.0, 'h21': -2991.6
 }
 
 def compute_reference_field(self, position: np.ndarray) -> np.ndarray:
 """Compute reference magnetic field at position."""
 lat, lon, alt = position
 
 # Simplified dipole model
 # In practice, use full IGRF spherical harmonic expansion
 declination = 0.0 # Would come from IGRF
 inclination = 60.0 # Would come from IGRF
 intensity = 50000.0 # nT, would come from IGRF
 
 return np.array([
 intensity * np.cos(np.radians(inclination)) * np.cos(np.radians(declination)),
 intensity * np.cos(np.radians(inclination)) * np.sin(np.radians(declination)),
 intensity * np.sin(np.radians(inclination))
 ])
 
 def match_position(self, measured_field: np.ndarray,
 search_area: Tuple[np.ndarray, np.ndarray]) -> np.ndarray:
 """Find position that best matches magnetic measurements."""
 min_pos, max_pos = search_area
 
 best_position = (min_pos + max_pos) / 2
 best_error = float('inf')
 
 step = 100.0 # meters
 
 x = min_pos[0]
 while x <= max_pos[0]:
 y = min_pos[1]
 while y <= max_pos[1]:
 # Get map value
 map_value = self._get_map_value(x, y)
 
 if map_value is not None:
 error = np.linalg.norm(measured_field - map_value)
 
 if error < best_error:
 best_error = error
 best_position = np.array([x, y])
 
 y += step
 x += step
 
 return best_position
 
 def _get_map_value(self, x: float, y: float) -> np.ndarray:
 """Get magnetic value from map at position."""
 ix = int(x / self.resolution)
 iy = int(y / self.resolution)
 
 if 0 <= ix < self.magnetic_map.shape[1] and \
 0 <= iy < self.magnetic_map.shape[0]:
 return self.magnetic_map[iy, ix]
 return None
 
 def detect_anomaly(self, measured_field: np.ndarray,
 reference_field: np.ndarray) -> float:
 """Detect magnetic anomaly (difference from expected)."""
 anomaly = measured_field - reference_field
 return np.linalg.norm(anomaly)
 
 def kalman_filter_magnetic(self, measurements: List[np.ndarray],
 dt: float = 1.0) -> List[np.ndarray]:
 """Kalman filter for magnetic field estimation."""
 # State: [x, y, Bx, By, Bz]
 state = np.zeros(6)
 P = np.eye(6) * 100
 
 positions = []
 
 for z in measurements:
 # Predict (simple constant velocity)
 F = np.eye(6)
 F[0, 2] = dt
 F[1, 3] = dt
 
 state = F @ state
 P = F @ P @ F.T + np.eye(6) * 0.1
 
 # Update with magnetic measurement
 H = np.zeros((3, 6))
 H[0:3, 2:5] = np.eye(3)
 
 R = np.eye(3) * 100 # Measurement noise
 y = z - state[2:5]
 S = H @ P @ H.T + R
 K = P @ H.T @ np.linalg.inv(S)
 
 state = state + K @ y
 P = (np.eye(6) - K @ H) @ P
 
 positions.append(state[:2].copy())
 
 return positions

Celestial Navigation

Advertisement

Celestial navigation uses sun, moon, and star positions to determine orientation and approximate position.

python
from datetime import datetime, timedelta
from typing import Tuple

class CelestialNavigation:
 def __init__(self):
 # Solar position constants
 self_speed = 360 / 365.25 # degrees per day
 
 def compute_sun_position(self, datetime_utc: datetime,
 latitude: float, longitude: float) -> Tuple[float, float]:
 """Compute sun azimuth and elevation."""
 # Day of year
 day_of_year = datetime_utc.timetuple().tm_yday
 
 # Solar declination
 declination = 23.45 * np.sin(np.radians(360/365 * (day_of_year - 81)))
 
 # Hour angle
 solar_noon = 12.0 # Approximate
 hours_since_noon = datetime_utc.hour + datetime_utc.minute/60 - solar_noon
 hour_angle = hours_since_noon * 15 # 15 degrees per hour
 
 # Solar elevation
 lat_rad = np.radians(latitude)
 dec_rad = np.radians(declination)
 ha_rad = np.radians(hour_angle)
 
 elevation = np.arcsin(
 np.sin(lat_rad) * np.sin(dec_rad) +
 np.cos(lat_rad) * np.cos(dec_rad) * np.cos(ha_rad)
 )
 
 # Solar azimuth
 azimuth = np.arctan2(
 -np.sin(ha_rad),
 np.tan(dec_rad) * np.cos(lat_rad) - np.sin(lat_rad) * np.cos(ha_rad)
 )
 
 return np.degrees(azimuth), np.degrees(elevation)
 
 def determine_heading_from_sun(self, datetime_utc: datetime,
 latitude: float, longitude: float,
 measured_elevation: float) -> float:
 """Determine heading from sun elevation measurement."""
 # Get expected sun position
 _, expected_elevation = self.compute_sun_position(
 datetime_utc, latitude, longitude
 )
 
 # Heading correction based on elevation difference
 # This is simplified - real implementation uses full spherical astronomy
 heading_correction = measured_elevation - expected_elevation
 
 return heading_correction
 
 def star_tracker_heading(self, star_positions: list,
 catalog_stars: dict) -> float:
 """Determine heading from star observations."""
 if len(star_positions) < 2:
 return None
 
 # Match observed stars to catalog
 matched_stars = []
 for obs_pos in star_positions:
 for star_id, catalog_pos in catalog_stars.items():
 angular_distance = self._angular_distance(obs_pos, catalog_pos)
 if angular_distance < np.radians(5): # 5 degree tolerance
 matched_stars.append((obs_pos, catalog_pos))
 break
 
 if len(matched_stars) < 2:
 return None
 
 # Compute rotation between observed and catalog frames
 # Using the two best matches
 obs1, cat1 = matched_stars[0]
 obs2, cat2 = matched_stars[1]
 
 # Compute angle differences
 obs_angle = np.arctan2(obs1[1] - obs2[1], obs1[0] - obs2[0])
 cat_angle = np.arctan2(cat1[1] - cat2[1], cat1[0] - cat2[0])
 
 heading = cat_angle - obs_angle
 
 return np.degrees(heading) % 360
 
 def _angular_distance(self, pos1, pos2):
 """Compute angular distance between two sky positions."""
 return np.arccos(np.clip(
 np.sin(pos1[1]) * np.sin(pos2[1]) +
 np.cos(pos1[1]) * np.cos(pos2[1]) * np.cos(pos1[0] - pos2[0]),
 -1, 1
 ))

class StarCatalog:
 """Manage star catalog for celestial navigation."""
 
 def __init__(self):
 self.bright_stars = {
 'Polaris': (np.radians(37.95), np.radians(89.26)),
 'Sirius': (np.radians(101.29), np.radians(-16.72)),
 'Canopus': (np.radians(95.99), np.radians(-52.70)),
 'Arcturus': (np.radians(213.92), np.radians(19.18)),
 'Vega': (np.radians(279.23), np.radians(38.78)),
 'Capella': (np.radians(79.17), np.radians(46.00)),
 'Rigel': (np.radians(78.63), np.radians(-8.20)),
 'Procyon': (np.radians(114.83), np.radians(5.23)),
 }
 
 def get_reference_stars(self, min_elevation: float = 30.0) -> dict:
 """Get stars suitable for navigation."""
 return self.bright_stars

Multi-Sensor Fusion for GPS-Denied Navigation

python
from typing import Dict, List, Optional
from dataclasses import dataclass

@dataclass
class NavigationState:
 position: np.ndarray
 velocity: np.ndarray
 orientation: np.ndarray
 covariance: np.ndarray
 confidence: float

class GPSDeniedNavigationSystem:
 def __init__(self):
 self.vio = None # Visual-Inertial Odometry
 self.terrain_nav = None # Terrain navigation
 self.magnetic_nav = None # Magnetic navigation
 self.celestial_nav = CelestialNavigation()
 
 self.state = NavigationState(
 position=np.zeros(3),
 velocity=np.zeros(3),
 orientation=np.zeros(3),
 covariance=np.eye(6) * 1.0,
 confidence=1.0
 )
 
 self.available_sensors = set()
 self.sensor_weights = {
 'vio': 0.4,
 'terrain': 0.3,
 'magnetic': 0.2,
 'celestial': 0.1
 }
 
 def update_sensor_availability(self, sensors: Dict[str, bool]):
 """Update which sensors are currently available."""
 for sensor, available in sensors.items():
 if available:
 self.available_sensors.add(sensor)
 else:
 self.available_sensors.discard(sensor)
 
 def fuse_measurements(self, measurements: Dict[str, np.ndarray]) -> NavigationState:
 """Fuse measurements from available sensors."""
 positions = []
 weights = []
 
 # VIO
 if 'vio' in measurements and 'vio' in self.available_sensors:
 vio_pos = measurements['vio']
 positions.append(vio_pos)
 weights.append(self.sensor_weights['vio'])
 
 # Terrain
 if 'terrain' in measurements and 'terrain' in self.available_sensors:
 terrain_pos = measurements['terrain']
 positions.append(terrain_pos)
 weights.append(self.sensor_weights['terrain'])
 
 # Magnetic
 if 'magnetic' in measurements and 'magnetic' in self.available_sensors:
 magnetic_pos = measurements['magnetic']
 positions.append(magnetic_pos)
 weights.append(self.sensor_weights['magnetic'])
 
 # Celestial
 if 'celestial' in measurements and 'celestial' in self.available_sensors:
 celestial_pos = measurements['celestial']
 positions.append(celestial_pos)
 weights.append(self.sensor_weights['celestial'])
 
 if not positions:
 return self.state
 
 # Normalize weights
 weights = np.array(weights)
 weights = weights / weights.sum()
 
 # Weighted fusion
 positions = np.array(positions)
 fused_position = np.average(positions, axis=0, weights=weights)
 
 # Update covariance
 if len(positions) > 1:
 position_variance = np.var(positions, axis=0)
 self.state.covariance[:3, :3] = np.diag(position_variance)
 
 # Update state
 self.state.position = fused_position
 self.state.confidence = 1.0 - np.sqrt(np.mean(self.state.covariance[:3, :3]))
 
 return self.state
 
 def predict_position(self, dt: float, control_input: np.ndarray) -> np.ndarray:
 """Dead reckoning prediction when no measurements available."""
 # Simple constant velocity model
 self.state.position += self.state.velocity * dt
 self.state.velocity += control_input * dt
 
 # Increase uncertainty
 self.state.covariance += np.eye(6) * 0.1 * dt
 
 self.state.confidence *= 0.99 # Decay confidence
 
 return self.state.position.copy()
 
 def get_navigation_solution(self) -> Dict:
 """Get current navigation solution with quality metrics."""
 return {
 'position': self.state.position.copy(),
 'velocity': self.state.velocity.copy(),
 'orientation': self.state.orientation.copy(),
 'position_uncertainty': np.sqrt(np.diag(self.state.covariance[:3, :3])),
 'confidence': self.state.confidence,
 'available_sensors': list(self.available_sensors),
 'recommended_action': self._get_recommendation()
 }
 
 def _get_recommendation(self) -> str:
 """Recommend action based on navigation quality."""
 if self.state.confidence > 0.8:
 return 'NORMAL_OPERATION'
 elif self.state.confidence > 0.5:
 return 'REDUCE_SPEED'
 elif self.state.confidence > 0.3:
 return 'INCREASE_ALTITUDE'
 else:
 return 'CONSIDER_RTL'

Hands-On Project: Complete GPS-Denied Navigation

python
from typing import Dict, List

class GPSDeniedDrone:
 def __init__(self, environment: str = 'indoor'):
 self.environment = environment
 
 # Navigation systems based on environment
 self.nav_system = GPSDeniedNavigationSystem()
 
 # Environment-specific configurations
 self.config = self._get_config(environment)
 
 # State
 self.position = np.zeros(3)
 self.velocity = np.zeros(3)
 self.heading = 0.0
 
 # Logs
 self.navigation_log = []
 
 def _get_config(self, environment: str) -> Dict:
 configs = {
 'indoor': {
 'sensors': ['vio', 'uwb', 'wifi'],
 'update_rate': 30,
 'max_speed': 2.0
 },
 'urban': {
 'sensors': ['vio', 'magnetic', 'visual_landmarks'],
 'update_rate': 10,
 'max_speed': 5.0
 },
 'cave': {
 'sensors': ['vio', 'magnetic'],
 'update_rate': 10,
 'max_speed': 1.0
 },
 'forest': {
 'sensors': ['vio', 'terrain', 'magnetic'],
 'update_rate': 5,
 'max_speed': 3.0
 }
 }
 return configs.get(environment, configs['indoor'])
 
 def process_sensor_data(self, sensor_data: Dict) -> Dict:
 """Process sensor data and update navigation."""
 measurements = {}
 
 # Process each available sensor
 for sensor in self.config['sensors']:
 if sensor in sensor_data:
 if sensor == 'vio':
 measurements['vio'] = self._process_vio(sensor_data[sensor])
 elif sensor == 'uwb':
 measurements['uwb'] = self._process_uwb(sensor_data[sensor])
 elif sensor == 'magnetic':
 measurements['magnetic'] = self._process_magnetic(sensor_data[sensor])
 elif sensor == 'terrain':
 measurements['terrain'] = self._process_terrain(sensor_data[sensor])
 
 # Fuse measurements
 if measurements:
 state = self.nav_system.fuse_measurements(measurements)
 self.position = state.position
 self.velocity = state.velocity
 
 # Log
 self.navigation_log.append({
 'position': self.position.copy(),
 'sensors_used': list(measurements.keys()),
 'confidence': self.nav_system.state.confidence
 })
 
 return {
 'position': self.position,
 'velocity': self.velocity,
 'heading': self.heading,
 'confidence': self.nav_system.state.confidence
 }
 
 def _process_vio(self, data: Dict) -> np.ndarray:
 """Process VIO data."""
 return data.get('position', self.position)
 
 def _process_uwb(self, data: Dict) -> np.ndarray:
 """Process UWB ranging data."""
 return data.get('position', self.position)
 
 def _process_magnetic(self, data: Dict) -> np.ndarray:
 """Process magnetic field data."""
 return data.get('position', self.position)
 
 def _process_terrain(self, data: Dict) -> np.ndarray:
 """Process terrain profile data."""
 return data.get('position', self.position)
 
 def navigate_to(self, target: np.ndarray) -> Dict:
 """Navigate to target in GPS-denied environment."""
 error = target - self.position
 distance = np.linalg.norm(error)
 
 if distance < 0.5:
 return {'status': 'arrived', 'position': self.position}
 
 # Velocity command
 speed = min(self.config['max_speed'], distance * 0.5)
 direction = error / distance
 velocity_cmd = direction * speed
 
 # Update velocity
 self.velocity = velocity_cmd
 
 return {
 'status': 'navigating',
 'velocity_command': velocity_cmd,
 'distance_to_target': distance,
 'position': self.position,
 'confidence': self.nav_system.state.confidence
 }
 
 def emergency_procedure(self) -> Dict:
 """Execute emergency procedure when navigation fails."""
 print("EMERGENCY: Navigation confidence critical!")
 
 # Try to find any available positioning
 available = self.nav_system.available_sensors
 
 if len(available) == 0:
 return {
 'action': 'HOVER_AND_AWAIT',
 'message': 'No positioning available - hovering'
 }
 
 # Land if possible
 return {
 'action': 'CONTROLLED_LANDING',
 'message': 'Attempting controlled landing'
 }
 
 def get_navigation_report(self) -> Dict:
 """Generate comprehensive navigation report."""
 report = {
 'current_position': self.position.tolist(),
 'heading': np.degrees(self.heading),
 'speed': np.linalg.norm(self.velocity),
 'confidence': self.nav_system.state.confidence,
 'sensors_active': list(self.nav_system.available_sensors),
 'position_uncertainty': np.sqrt(
 np.diag(self.nav_system.state.covariance[:3, :3])
 ).tolist(),
 'environment': self.environment,
 'navigation_log_length': len(self.navigation_log)
 }
 
 return report

def main():
 # Test GPS-denied navigation in different environments
 environments = ['indoor', 'urban', 'cave', 'forest']
 
 for env in environments:
 print(f"\n{'='*50}")
 print(f"Testing GPS-denied navigation: {env.upper()}")
 print('='*50)
 
 drone = GPSDeniedDrone(environment=env)
 
 # Simulate navigation
 target = np.array([10.0, 10.0, 2.0])
 
 for step in range(20):
 result = drone.navigate_to(target)
 
 # Simulate sensor data
 sensor_data = {
 'vio': {'position': drone.position + np.random.randn(3) * 0.1},
 }
 
 nav_state = drone.process_sensor_data(sensor_data)
 
 print(f"Step {step}: Position={nav_state['position'][:2]}, "
 f"Confidence={nav_state['confidence']:.2f}")
 
 if result['status'] == 'arrived':
 print("Target reached!")
 break
 
 # Print report
 report = drone.get_navigation_report()
 print(f"\nNavigation Report:")
 print(f" Final position: {report['current_position'][:2]}")
 print(f" Sensors used: {report['sensors_active']}")

if __name__ == "__main__":
 main()

Key Takeaways

  • VIO provides short-term accurate odometry but drifts over time
  • Terrain matching gives bounded-error position fixes outdoors
  • Magnetic navigation works in any environment with a magnetic map
  • Celestial navigation serves as a backup when other sensors fail
  • Multi-sensor fusion provides robust navigation across environments
  • Always monitor navigation confidence and have fallback procedures
—
☆☆☆☆☆
0 ratings

Rate & Feedback

Need Expert Drone AI Help?

Get personalized tutoring, project support, or professional consulting.

Advertisement