Files
Python/geodesy/radar_target_calculation.py
deniztunccekicdeniztunccekicpre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1d68f8e975 Add ENU ECEF target coordinate calculation (geodesy) (#15056)
* Add radar target coordinate calculation

* Move radar target calculation to geodesy folder

* Add Wikipedia references to comply with PR template

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fix E501 line too long in docstring

* Rename variables to descriptive names to pass algorithms-keeper

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fix doctest expected value to pass build tests

---------

Co-authored-by: deniztunccekic <deniztunccekic@example.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-09-15 14:37:42 +02:00

181 lines
5.9 KiB
Python

"""
This module provides functions to convert between Geodetic coordinates and
Earth-Centered, Earth-Fixed (ECEF) Cartesian coordinates, as well as calculating
target coordinates based on radar measurements.
Reference:
- https://en.wikipedia.org/wiki/Geographic_coordinate_conversion
- https://en.wikipedia.org/wiki/Local_tangent_plane_coordinates
"""
import math
# WGS84 Ellipsoid Constants
WGS84_A = 6378137.0 # Semi-major axis in meters
WGS84_B = 6356752.314245 # Semi-minor axis in meters
WGS84_E_SQ = 1.0 - (WGS84_B**2 / WGS84_A**2) # First eccentricity squared
WGS84_EP_SQ = (WGS84_A**2 - WGS84_B**2) / WGS84_B**2 # Second eccentricity squared
def geodetic_to_ecef(
lat_deg: float, lon_deg: float, alt_m: float
) -> tuple[float, float, float]:
"""
Converts Geodetic coordinates (Latitude, Longitude, Altitude) to
Earth-Centered, Earth-Fixed (ECEF) Cartesian coordinates.
>>> x, y, z = geodetic_to_ecef(0.0, 0.0, 0.0)
>>> round(x, 2), round(y, 2), round(z, 2)
(6378137.0, 0.0, 0.0)
>>> x, y, z = geodetic_to_ecef(90.0, 0.0, 0.0)
>>> round(x, 2), round(y, 2), round(z, 2)
(0.0, 0.0, 6356752.31)
"""
lat_rad = math.radians(lat_deg)
lon_rad = math.radians(lon_deg)
sin_lat = math.sin(lat_rad)
cos_lat = math.cos(lat_rad)
# N is the prime vertical radius of curvature
n_radius = WGS84_A / math.sqrt(1.0 - WGS84_E_SQ * sin_lat**2)
# Calculate ECEF X, Y, Z
x = (n_radius + alt_m) * cos_lat * math.cos(lon_rad)
y = (n_radius + alt_m) * cos_lat * math.sin(lon_rad)
z = (n_radius * (1.0 - WGS84_E_SQ) + alt_m) * sin_lat
return x, y, z
def ecef_to_geodetic(
x_ecef: float, y_ecef: float, z_ecef: float
) -> tuple[float, float, float]:
"""
Converts Earth-Centered, Earth-Fixed (ECEF) coordinates to
Geodetic coordinates (Latitude, Longitude, Altitude) using Bowring's method.
>>> lat, lon, alt = ecef_to_geodetic(6378137.0, 0.0, 0.0)
>>> round(lat, 2), round(lon, 2), round(alt, 2)
(0.0, 0.0, 0.0)
>>> lat, lon, alt = ecef_to_geodetic(0.0, 0.0, 6356752.314245)
>>> round(lat, 2), round(lon, 2), round(alt, 2)
(90.0, 0.0, 0.0)
"""
p = math.sqrt(x_ecef**2 + y_ecef**2)
# Handle the special case where the point is exactly at the poles
if p == 0:
lon_deg = 0.0
lat_deg = 90.0 if z_ecef > 0 else -90.0
alt_m = abs(z_ecef) - WGS84_B
return lat_deg, lon_deg, alt_m
theta = math.atan2(z_ecef * WGS84_A, p * WGS84_B)
sin_theta = math.sin(theta)
cos_theta = math.cos(theta)
# Calculate exact latitude and longitude
lon_rad = math.atan2(y_ecef, x_ecef)
lat_rad = math.atan2(
z_ecef + WGS84_EP_SQ * WGS84_B * sin_theta**3,
p - WGS84_E_SQ * WGS84_A * cos_theta**3,
)
sin_lat = math.sin(lat_rad)
# Recalculate prime vertical radius to find altitude
n_radius = WGS84_A / math.sqrt(1.0 - WGS84_E_SQ * sin_lat**2)
alt_m = (p / math.cos(lat_rad)) - n_radius
return math.degrees(lat_rad), math.degrees(lon_rad), alt_m
def enu_to_ecef(
east: float, north: float, up: float, ref_lat_deg: float, ref_lon_deg: float
) -> tuple[float, float, float]:
"""
Rotates East-North-Up (ENU) offset coordinates to ECEF offset coordinates,
based on the reference (Radar) latitude and longitude.
>>> dx, dy, dz = enu_to_ecef(100.0, 200.0, 50.0, 0.0, 0.0)
>>> round(dx, 2), round(dy, 2), round(dz, 2)
(50.0, 100.0, 200.0)
"""
lat_rad = math.radians(ref_lat_deg)
lon_rad = math.radians(ref_lon_deg)
sin_lat = math.sin(lat_rad)
cos_lat = math.cos(lat_rad)
sin_lon = math.sin(lon_rad)
cos_lon = math.cos(lon_rad)
# Rotation matrix components for ENU to ECEF
dx = -sin_lon * east - sin_lat * cos_lon * north + cos_lat * cos_lon * up
dy = cos_lon * east - sin_lat * sin_lon * north + cos_lat * sin_lon * up
dz = cos_lat * north + sin_lat * up
return dx, dy, dz
def calculate_target_coordinates(
radar_lat: float,
radar_lon: float,
radar_alt: float,
azimuth_deg: float,
range_m: float,
elevation_deg: float = 0.0,
) -> tuple[float, float, float]:
"""
Main function to calculate target (ship) coordinates from radar measurements.
Parameters:
radar_lat (float): Radar latitude in degrees
radar_lon (float): Radar longitude in degrees
radar_alt (float): Radar altitude above sea level in meters
azimuth_deg (float): True bearing to the target (0 is North, 90 is East)
range_m (float): Direct line-of-sight distance to the target in meters
elevation_deg (float): Antenna elevation angle in degrees
(default 0 for surface ships)
Returns:
tuple: (Target Latitude, Target Longitude, Target Altitude)
>>> lat, lon, alt = calculate_target_coordinates(0.0, 0.0, 0.0, 90.0, 111319.5)
>>> round(lat, 1), round(lon, 1), round(alt, 1)
(0.0, 1.0, 971.4)
"""
# Step 1: Convert Radar polar measurements to Local ENU Cartesian coordinates
az_rad = math.radians(azimuth_deg)
el_rad = math.radians(elevation_deg)
# Standard spherical to cartesian for ENU
# North is aligned with 0 degrees Azimuth, East is 90 degrees
east = range_m * math.cos(el_rad) * math.sin(az_rad)
north = range_m * math.cos(el_rad) * math.cos(az_rad)
up = range_m * math.sin(el_rad)
# Step 2: Get absolute ECEF position of the Radar
radar_x, radar_y, radar_z = geodetic_to_ecef(radar_lat, radar_lon, radar_alt)
# Step 3: Convert the Local ENU offsets to ECEF offsets
dx, dy, dz = enu_to_ecef(east, north, up, radar_lat, radar_lon)
# Step 4: Add offsets to the Radar's ECEF coordinates to find Target ECEF
target_x = radar_x + dx
target_y = radar_y + dy
target_z = radar_z + dz
# Step 5: Convert Target ECEF back to Geodetic coordinates
target_lat, target_lon, target_alt = ecef_to_geodetic(target_x, target_y, target_z)
return target_lat, target_lon, target_alt
if __name__ == "__main__":
import doctest
doctest.testmod()